oh-my-customcode 0.73.0 → 0.75.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 +13 -10
- package/dist/cli/index.js +358 -56
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/.claude/skills/analysis/SKILL.md +43 -3
- package/templates/manifest.json +1 -1
- package/templates/workflows/auto-dev.yaml +10 -8
package/README.md
CHANGED
|
@@ -21,17 +21,16 @@ npm install -g oh-my-customcode && cd your-project && omcustom init
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
-
## What's New in v0.
|
|
24
|
+
## What's New in v0.74.0
|
|
25
25
|
|
|
26
26
|
| Feature | Description |
|
|
27
27
|
|---------|-------------|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
| **
|
|
32
|
-
| **
|
|
33
|
-
| **
|
|
34
|
-
| **CC v2.1.83–v2.1.87 Compat** | Conditional hook `if` field, CwdChanged/FileChanged events, managed-settings.d |
|
|
28
|
+
| **`omcustom sync`** | Drift detection for `.claude/` configuration — compare against lockfile, export team snapshots |
|
|
29
|
+
| **`omcustom init --from-snapshot`** | Team reproducibility — install from pre-configured snapshot directory |
|
|
30
|
+
| **`analysis --interview`** | Interactive AI architecture interview before file-based project detection |
|
|
31
|
+
| **skill-extractor** | 100th skill — analyze task trajectories to propose reusable SKILL.md candidates |
|
|
32
|
+
| **User Model** | Structured tracking of correction patterns, skill preferences, expertise profile |
|
|
33
|
+
| **Release Cleanup** | Auto-close linked issues and delete release branches on PR merge |
|
|
35
34
|
|
|
36
35
|
---
|
|
37
36
|
|
|
@@ -153,13 +152,13 @@ Each agent declares its tools, model, memory scope, and limitations in YAML fron
|
|
|
153
152
|
| Best Practices | 24 | Go, Python, TypeScript, Kotlin, Rust, React, FastAPI, Spring Boot, Django, Flutter, Docker, AWS, Postgres, Redis, Kafka, dbt, Spark, Snowflake, Airflow, pipeline-architecture-patterns, alembic, and more |
|
|
154
153
|
| Routing | 4 | secretary, dev-lead, de-lead, qa-lead |
|
|
155
154
|
| Workflow | 13 | structured-dev-cycle, deep-plan, research, evaluator-optimizer, dag-orchestration, worker-reviewer-pipeline, reasoning-sandwich, pipeline, and more |
|
|
156
|
-
| Development |
|
|
155
|
+
| Development | 8 | dev-review, dev-refactor, analysis, create-agent, intent-detection, web-design-guidelines, omcustom-takeover, skill-extractor |
|
|
157
156
|
| Operations | 9 | update-docs, audit-agents, sauron-watch, monitoring-setup, fix-refs, release-notes, and more |
|
|
158
157
|
| Memory | 3 | memory-save, memory-recall, memory-management |
|
|
159
158
|
| Package | 3 | npm-publish, npm-version, npm-audit |
|
|
160
159
|
| Optimization | 3 | optimize-analyze, optimize-bundle, optimize-report |
|
|
161
160
|
| Security | 3 | adversarial-review, cve-triage, jinja2-prompts |
|
|
162
|
-
| Other |
|
|
161
|
+
| Other | 10 | codex-exec, claude-native, vercel-deploy, skills-sh-search, result-aggregation, writing-clearly-and-concisely, and more |
|
|
163
162
|
|
|
164
163
|
Skills use a 3-tier scope system: `core` (universal), `harness` (agent/skill maintenance), `package` (project-specific).
|
|
165
164
|
|
|
@@ -262,6 +261,10 @@ Security hooks are advisory (exit 0). They warn but never block.
|
|
|
262
261
|
```bash
|
|
263
262
|
omcustom init # Interactive setup wizard (language, framework, team mode)
|
|
264
263
|
omcustom init --lang ko # Initialize with Korean
|
|
264
|
+
omcustom init --from-snapshot # Install from pre-configured team snapshot
|
|
265
|
+
omcustom sync # Detect drift between .claude/ state and lockfile
|
|
266
|
+
omcustom sync --check # Check for drift without applying changes
|
|
267
|
+
omcustom sync --export # Export current state as team snapshot
|
|
265
268
|
omcustom update # Update to latest
|
|
266
269
|
omcustom list # List components
|
|
267
270
|
omcustom doctor # Verify installation
|
package/dist/cli/index.js
CHANGED
|
@@ -9325,7 +9325,7 @@ var init_package = __esm(() => {
|
|
|
9325
9325
|
workspaces: [
|
|
9326
9326
|
"packages/*"
|
|
9327
9327
|
],
|
|
9328
|
-
version: "0.
|
|
9328
|
+
version: "0.75.0",
|
|
9329
9329
|
description: "Batteries-included agent harness for Claude Code",
|
|
9330
9330
|
type: "module",
|
|
9331
9331
|
bin: {
|
|
@@ -11185,13 +11185,13 @@ var PromisePolyfill;
|
|
|
11185
11185
|
var init_promise_polyfill = __esm(() => {
|
|
11186
11186
|
PromisePolyfill = class PromisePolyfill extends Promise {
|
|
11187
11187
|
static withResolver() {
|
|
11188
|
-
let
|
|
11188
|
+
let resolve3;
|
|
11189
11189
|
let reject;
|
|
11190
11190
|
const promise = new Promise((res, rej) => {
|
|
11191
|
-
|
|
11191
|
+
resolve3 = res;
|
|
11192
11192
|
reject = rej;
|
|
11193
11193
|
});
|
|
11194
|
-
return { promise, resolve:
|
|
11194
|
+
return { promise, resolve: resolve3, reject };
|
|
11195
11195
|
}
|
|
11196
11196
|
};
|
|
11197
11197
|
});
|
|
@@ -11229,7 +11229,7 @@ function createPrompt(view) {
|
|
|
11229
11229
|
output
|
|
11230
11230
|
});
|
|
11231
11231
|
const screen = new ScreenManager(rl);
|
|
11232
|
-
const { promise, resolve:
|
|
11232
|
+
const { promise, resolve: resolve3, reject } = PromisePolyfill.withResolver();
|
|
11233
11233
|
const cancel = () => reject(new CancelPromptError);
|
|
11234
11234
|
if (signal) {
|
|
11235
11235
|
const abort = () => reject(new AbortPromptError({ cause: signal.reason }));
|
|
@@ -11257,7 +11257,7 @@ function createPrompt(view) {
|
|
|
11257
11257
|
cycle(() => {
|
|
11258
11258
|
try {
|
|
11259
11259
|
const nextView = view(config, (value) => {
|
|
11260
|
-
setImmediate(() =>
|
|
11260
|
+
setImmediate(() => resolve3(value));
|
|
11261
11261
|
});
|
|
11262
11262
|
if (nextView === undefined) {
|
|
11263
11263
|
const callerFilename = callSites[1]?.getFileName();
|
|
@@ -17122,7 +17122,7 @@ var require_lib2 = __commonJS((exports) => {
|
|
|
17122
17122
|
return matches;
|
|
17123
17123
|
};
|
|
17124
17124
|
exports.analyse = analyse;
|
|
17125
|
-
var detectFile = (filepath, opts = {}) => new Promise((
|
|
17125
|
+
var detectFile = (filepath, opts = {}) => new Promise((resolve3, reject) => {
|
|
17126
17126
|
let fd;
|
|
17127
17127
|
const fs3 = (0, node_1.default)();
|
|
17128
17128
|
const handler = (err, buffer) => {
|
|
@@ -17132,7 +17132,7 @@ var require_lib2 = __commonJS((exports) => {
|
|
|
17132
17132
|
if (err) {
|
|
17133
17133
|
reject(err);
|
|
17134
17134
|
} else if (buffer) {
|
|
17135
|
-
|
|
17135
|
+
resolve3((0, exports.detect)(buffer));
|
|
17136
17136
|
} else {
|
|
17137
17137
|
reject(new Error("No error and no buffer received"));
|
|
17138
17138
|
}
|
|
@@ -24757,6 +24757,9 @@ var en_default = {
|
|
|
24757
24757
|
initializingTemplate: "Initializing with template: {{name}}",
|
|
24758
24758
|
promptOverwrite: "Configuration already exists. Overwrite?",
|
|
24759
24759
|
aborted: "Initialization aborted",
|
|
24760
|
+
snapshot: {
|
|
24761
|
+
installing: "Installing from team snapshot..."
|
|
24762
|
+
},
|
|
24760
24763
|
wizard: {
|
|
24761
24764
|
welcome: "Welcome to oh-my-customcode setup!",
|
|
24762
24765
|
langPrompt: "Select your preferred language",
|
|
@@ -24970,6 +24973,9 @@ var en_default = {
|
|
|
24970
24973
|
serveStop: "[Deprecated] `omcustom serve-stop` is deprecated. Use `omcustom web stop` instead."
|
|
24971
24974
|
}
|
|
24972
24975
|
},
|
|
24976
|
+
sync: {
|
|
24977
|
+
description: "Check .claude/ configuration drift or export snapshot"
|
|
24978
|
+
},
|
|
24973
24979
|
security: {
|
|
24974
24980
|
description: "Scan for security issues in hooks, configs, and templates",
|
|
24975
24981
|
verboseOption: "Show detailed scan results",
|
|
@@ -25164,6 +25170,9 @@ var ko_default = {
|
|
|
25164
25170
|
initializingTemplate: "템플릿으로 초기화 중: {{name}}",
|
|
25165
25171
|
promptOverwrite: "설정 파일이 이미 존재합니다. 덮어쓰시겠습니까?",
|
|
25166
25172
|
aborted: "초기화가 취소되었습니다",
|
|
25173
|
+
snapshot: {
|
|
25174
|
+
installing: "팀 스냅샷에서 설치 중..."
|
|
25175
|
+
},
|
|
25167
25176
|
wizard: {
|
|
25168
25177
|
welcome: "oh-my-customcode 설정을 시작합니다!",
|
|
25169
25178
|
langPrompt: "사용할 언어를 선택하세요",
|
|
@@ -25377,6 +25386,9 @@ var ko_default = {
|
|
|
25377
25386
|
serveStop: "[Deprecated] `omcustom serve-stop` is deprecated. Use `omcustom web stop` instead."
|
|
25378
25387
|
}
|
|
25379
25388
|
},
|
|
25389
|
+
sync: {
|
|
25390
|
+
description: ".claude/ 설정 드리프트 확인 또는 스냅샷 내보내기"
|
|
25391
|
+
},
|
|
25380
25392
|
security: {
|
|
25381
25393
|
description: "훅, 설정, 템플릿의 보안 문제 검사",
|
|
25382
25394
|
verboseOption: "상세 검사 결과 표시",
|
|
@@ -26491,6 +26503,29 @@ async function generateAndWriteLockfileForDir(targetDir) {
|
|
|
26491
26503
|
return { fileCount: 0, warning: `Lockfile generation failed: ${msg}` };
|
|
26492
26504
|
}
|
|
26493
26505
|
}
|
|
26506
|
+
function diffLockfiles(base, current) {
|
|
26507
|
+
const baseKeys = new Set(Object.keys(base.files));
|
|
26508
|
+
const currentKeys = new Set(Object.keys(current.files));
|
|
26509
|
+
const added = [];
|
|
26510
|
+
const removed = [];
|
|
26511
|
+
const modified = [];
|
|
26512
|
+
const unchanged = [];
|
|
26513
|
+
for (const key of currentKeys) {
|
|
26514
|
+
if (!baseKeys.has(key)) {
|
|
26515
|
+
added.push(key);
|
|
26516
|
+
} else if (base.files[key].templateHash !== current.files[key].templateHash) {
|
|
26517
|
+
modified.push(key);
|
|
26518
|
+
} else {
|
|
26519
|
+
unchanged.push(key);
|
|
26520
|
+
}
|
|
26521
|
+
}
|
|
26522
|
+
for (const key of baseKeys) {
|
|
26523
|
+
if (!currentKeys.has(key)) {
|
|
26524
|
+
removed.push(key);
|
|
26525
|
+
}
|
|
26526
|
+
}
|
|
26527
|
+
return { added, removed, modified, unchanged };
|
|
26528
|
+
}
|
|
26494
26529
|
|
|
26495
26530
|
// src/core/rtk-installer.ts
|
|
26496
26531
|
import { execSync as execSync4 } from "node:child_process";
|
|
@@ -27209,7 +27244,7 @@ async function doctorCommand(options = {}) {
|
|
|
27209
27244
|
|
|
27210
27245
|
// src/cli/init.ts
|
|
27211
27246
|
init_package();
|
|
27212
|
-
import { join as
|
|
27247
|
+
import { join as join11 } from "node:path";
|
|
27213
27248
|
|
|
27214
27249
|
// src/core/installer.ts
|
|
27215
27250
|
init_fs();
|
|
@@ -28111,6 +28146,82 @@ async function checkUvAvailable() {
|
|
|
28111
28146
|
}
|
|
28112
28147
|
}
|
|
28113
28148
|
|
|
28149
|
+
// src/core/snapshot.ts
|
|
28150
|
+
init_package();
|
|
28151
|
+
init_projects();
|
|
28152
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
28153
|
+
import { copyFile as copyFile2, cp } from "node:fs/promises";
|
|
28154
|
+
import { join as join10 } from "node:path";
|
|
28155
|
+
init_fs();
|
|
28156
|
+
async function checkExistingInstallation(targetDir) {
|
|
28157
|
+
const layout = getProviderLayout();
|
|
28158
|
+
const rootDir = join10(targetDir, layout.rootDir);
|
|
28159
|
+
return fileExists(rootDir);
|
|
28160
|
+
}
|
|
28161
|
+
async function installFromSnapshot(targetDir, snapshotPath, options) {
|
|
28162
|
+
if (!existsSync2(snapshotPath)) {
|
|
28163
|
+
return {
|
|
28164
|
+
success: false,
|
|
28165
|
+
message: i18n.t("cli.init.failed"),
|
|
28166
|
+
errors: [`Snapshot path not found: ${snapshotPath}`]
|
|
28167
|
+
};
|
|
28168
|
+
}
|
|
28169
|
+
const layout = getProviderLayout();
|
|
28170
|
+
const snapshotClaude = join10(snapshotPath, layout.rootDir);
|
|
28171
|
+
if (!existsSync2(snapshotClaude)) {
|
|
28172
|
+
return {
|
|
28173
|
+
success: false,
|
|
28174
|
+
message: i18n.t("cli.init.failed"),
|
|
28175
|
+
errors: [`Invalid snapshot: missing ${layout.rootDir}/ directory in ${snapshotPath}`]
|
|
28176
|
+
};
|
|
28177
|
+
}
|
|
28178
|
+
console.log(`Installing from snapshot: ${snapshotPath}`);
|
|
28179
|
+
try {
|
|
28180
|
+
const exists2 = await checkExistingInstallation(targetDir);
|
|
28181
|
+
if (exists2 && !options.force) {
|
|
28182
|
+
console.log(i18n.t("cli.init.exists", { rootDir: layout.rootDir }));
|
|
28183
|
+
console.log(i18n.t("cli.init.backing_up"));
|
|
28184
|
+
const backupDir = join10(targetDir, `.claude-backup-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, -1)}`);
|
|
28185
|
+
await cp(join10(targetDir, layout.rootDir), backupDir, { recursive: true });
|
|
28186
|
+
console.log(` Backed up to: ${backupDir}`);
|
|
28187
|
+
}
|
|
28188
|
+
await cp(snapshotClaude, join10(targetDir, layout.rootDir), {
|
|
28189
|
+
recursive: true,
|
|
28190
|
+
force: true
|
|
28191
|
+
});
|
|
28192
|
+
const snapshotGuides = join10(snapshotPath, "guides");
|
|
28193
|
+
if (existsSync2(snapshotGuides)) {
|
|
28194
|
+
await cp(snapshotGuides, join10(targetDir, "guides"), {
|
|
28195
|
+
recursive: true,
|
|
28196
|
+
force: true
|
|
28197
|
+
});
|
|
28198
|
+
}
|
|
28199
|
+
const snapshotEntry = join10(snapshotPath, layout.entryFile);
|
|
28200
|
+
if (existsSync2(snapshotEntry)) {
|
|
28201
|
+
await copyFile2(snapshotEntry, join10(targetDir, layout.entryFile));
|
|
28202
|
+
}
|
|
28203
|
+
try {
|
|
28204
|
+
const existing = await readLockFile(targetDir);
|
|
28205
|
+
await writeLockFile(targetDir, package_default.version, existing);
|
|
28206
|
+
} catch {}
|
|
28207
|
+
console.log(i18n.t("cli.init.success"));
|
|
28208
|
+
console.log(`
|
|
28209
|
+
Installed from snapshot: ${snapshotPath}`);
|
|
28210
|
+
return {
|
|
28211
|
+
success: true,
|
|
28212
|
+
message: `Installed from snapshot: ${snapshotPath}`
|
|
28213
|
+
};
|
|
28214
|
+
} catch (error2) {
|
|
28215
|
+
const errorMessage = error2 instanceof Error ? error2.message : String(error2);
|
|
28216
|
+
console.error(i18n.t("cli.init.failed"), errorMessage);
|
|
28217
|
+
return {
|
|
28218
|
+
success: false,
|
|
28219
|
+
message: i18n.t("cli.init.failed"),
|
|
28220
|
+
errors: [errorMessage]
|
|
28221
|
+
};
|
|
28222
|
+
}
|
|
28223
|
+
}
|
|
28224
|
+
|
|
28114
28225
|
// src/cli/init.ts
|
|
28115
28226
|
init_fs();
|
|
28116
28227
|
init_projects();
|
|
@@ -29090,9 +29201,9 @@ async function runInitWizard(options) {
|
|
|
29090
29201
|
}
|
|
29091
29202
|
|
|
29092
29203
|
// src/cli/init.ts
|
|
29093
|
-
async function
|
|
29204
|
+
async function checkExistingInstallation2(targetDir) {
|
|
29094
29205
|
const layout = getProviderLayout();
|
|
29095
|
-
const rootDir =
|
|
29206
|
+
const rootDir = join11(targetDir, layout.rootDir);
|
|
29096
29207
|
return fileExists(rootDir);
|
|
29097
29208
|
}
|
|
29098
29209
|
var PROVIDER_SUBDIR_COMPONENTS = new Set([
|
|
@@ -29106,13 +29217,13 @@ var PROVIDER_SUBDIR_COMPONENTS = new Set([
|
|
|
29106
29217
|
function componentToPath(targetDir, component) {
|
|
29107
29218
|
if (component === "entry-md") {
|
|
29108
29219
|
const layout = getProviderLayout();
|
|
29109
|
-
return
|
|
29220
|
+
return join11(targetDir, layout.entryFile);
|
|
29110
29221
|
}
|
|
29111
29222
|
if (PROVIDER_SUBDIR_COMPONENTS.has(component)) {
|
|
29112
29223
|
const layout = getProviderLayout();
|
|
29113
|
-
return
|
|
29224
|
+
return join11(targetDir, layout.rootDir, component);
|
|
29114
29225
|
}
|
|
29115
|
-
return
|
|
29226
|
+
return join11(targetDir, component);
|
|
29116
29227
|
}
|
|
29117
29228
|
function buildInstalledPaths(targetDir, components) {
|
|
29118
29229
|
return components.map((component) => componentToPath(targetDir, component));
|
|
@@ -29178,6 +29289,9 @@ async function setupMcpConfig(targetDir) {
|
|
|
29178
29289
|
}
|
|
29179
29290
|
async function initCommand(options) {
|
|
29180
29291
|
const targetDir = process.cwd();
|
|
29292
|
+
if (options.fromSnapshot) {
|
|
29293
|
+
return installFromSnapshot(targetDir, options.fromSnapshot, options);
|
|
29294
|
+
}
|
|
29181
29295
|
const resolved = await resolveOptions(options);
|
|
29182
29296
|
if (!resolved) {
|
|
29183
29297
|
return { success: false, message: i18n.t("cli.init.wizard.cancelled") };
|
|
@@ -29185,7 +29299,7 @@ async function initCommand(options) {
|
|
|
29185
29299
|
console.log(i18n.t("cli.init.start"));
|
|
29186
29300
|
try {
|
|
29187
29301
|
const layout = getProviderLayout();
|
|
29188
|
-
const exists2 = await
|
|
29302
|
+
const exists2 = await checkExistingInstallation2(targetDir);
|
|
29189
29303
|
if (exists2) {
|
|
29190
29304
|
console.log(i18n.t("cli.init.exists", { rootDir: layout.rootDir }));
|
|
29191
29305
|
console.log(i18n.t("cli.init.backing_up"));
|
|
@@ -29231,7 +29345,7 @@ async function initCommand(options) {
|
|
|
29231
29345
|
}
|
|
29232
29346
|
|
|
29233
29347
|
// src/cli/list.ts
|
|
29234
|
-
import { basename as basename4, dirname as dirname4, join as
|
|
29348
|
+
import { basename as basename4, dirname as dirname4, join as join12, relative as relative3 } from "node:path";
|
|
29235
29349
|
init_fs();
|
|
29236
29350
|
var ALLOWED_TOP_LEVEL_KEYS = new Set(["name", "type", "description", "version", "category"]);
|
|
29237
29351
|
function parseKeyValue(line) {
|
|
@@ -29296,12 +29410,12 @@ function extractAgentTypeFromFilename(filename) {
|
|
|
29296
29410
|
return prefixMap[prefix] || "unknown";
|
|
29297
29411
|
}
|
|
29298
29412
|
function extractSkillCategoryFromPath(skillPath, baseDir, rootDir) {
|
|
29299
|
-
const relativePath = relative3(
|
|
29413
|
+
const relativePath = relative3(join12(baseDir, rootDir, "skills"), skillPath);
|
|
29300
29414
|
const parts = relativePath.split("/").filter(Boolean);
|
|
29301
29415
|
return parts[0] || "unknown";
|
|
29302
29416
|
}
|
|
29303
29417
|
function extractGuideCategoryFromPath(guidePath, baseDir) {
|
|
29304
|
-
const relativePath = relative3(
|
|
29418
|
+
const relativePath = relative3(join12(baseDir, "guides"), guidePath);
|
|
29305
29419
|
const parts = relativePath.split("/").filter(Boolean);
|
|
29306
29420
|
return parts[0] || "unknown";
|
|
29307
29421
|
}
|
|
@@ -29395,7 +29509,7 @@ async function tryExtractMarkdownDescription(mdPath, options = {}) {
|
|
|
29395
29509
|
}
|
|
29396
29510
|
}
|
|
29397
29511
|
async function getAgents(targetDir, rootDir = ".claude", config) {
|
|
29398
|
-
const agentsDir =
|
|
29512
|
+
const agentsDir = join12(targetDir, rootDir, "agents");
|
|
29399
29513
|
if (!await fileExists(agentsDir))
|
|
29400
29514
|
return [];
|
|
29401
29515
|
try {
|
|
@@ -29423,7 +29537,7 @@ async function getAgents(targetDir, rootDir = ".claude", config) {
|
|
|
29423
29537
|
}
|
|
29424
29538
|
}
|
|
29425
29539
|
async function getSkills(targetDir, rootDir = ".claude", config) {
|
|
29426
|
-
const skillsDir =
|
|
29540
|
+
const skillsDir = join12(targetDir, rootDir, "skills");
|
|
29427
29541
|
if (!await fileExists(skillsDir))
|
|
29428
29542
|
return [];
|
|
29429
29543
|
try {
|
|
@@ -29433,7 +29547,7 @@ async function getSkills(targetDir, rootDir = ".claude", config) {
|
|
|
29433
29547
|
const skillMdFiles = await listFiles(skillsDir, { recursive: true, pattern: "SKILL.md" });
|
|
29434
29548
|
const skills = await Promise.all(skillMdFiles.map(async (skillMdPath) => {
|
|
29435
29549
|
const skillDir = dirname4(skillMdPath);
|
|
29436
|
-
const indexYamlPath =
|
|
29550
|
+
const indexYamlPath = join12(skillDir, "index.yaml");
|
|
29437
29551
|
const { description, version } = await tryReadIndexYamlMetadata(indexYamlPath);
|
|
29438
29552
|
const relativePath = relative3(targetDir, skillDir);
|
|
29439
29553
|
return {
|
|
@@ -29452,7 +29566,7 @@ async function getSkills(targetDir, rootDir = ".claude", config) {
|
|
|
29452
29566
|
}
|
|
29453
29567
|
}
|
|
29454
29568
|
async function getGuides(targetDir, config) {
|
|
29455
|
-
const guidesDir =
|
|
29569
|
+
const guidesDir = join12(targetDir, "guides");
|
|
29456
29570
|
if (!await fileExists(guidesDir))
|
|
29457
29571
|
return [];
|
|
29458
29572
|
try {
|
|
@@ -29479,7 +29593,7 @@ async function getGuides(targetDir, config) {
|
|
|
29479
29593
|
}
|
|
29480
29594
|
var RULE_PRIORITY_ORDER = { MUST: 0, SHOULD: 1, MAY: 2 };
|
|
29481
29595
|
async function getRules(targetDir, rootDir = ".claude", config) {
|
|
29482
|
-
const rulesDir =
|
|
29596
|
+
const rulesDir = join12(targetDir, rootDir, "rules");
|
|
29483
29597
|
if (!await fileExists(rulesDir))
|
|
29484
29598
|
return [];
|
|
29485
29599
|
try {
|
|
@@ -29551,7 +29665,7 @@ function formatAsJson(components) {
|
|
|
29551
29665
|
console.log(JSON.stringify(components, null, 2));
|
|
29552
29666
|
}
|
|
29553
29667
|
async function getHooks(targetDir, rootDir = ".claude") {
|
|
29554
|
-
const hooksDir =
|
|
29668
|
+
const hooksDir = join12(targetDir, rootDir, "hooks");
|
|
29555
29669
|
if (!await fileExists(hooksDir))
|
|
29556
29670
|
return [];
|
|
29557
29671
|
try {
|
|
@@ -29569,7 +29683,7 @@ async function getHooks(targetDir, rootDir = ".claude") {
|
|
|
29569
29683
|
}
|
|
29570
29684
|
}
|
|
29571
29685
|
async function getContexts(targetDir, rootDir = ".claude") {
|
|
29572
|
-
const contextsDir =
|
|
29686
|
+
const contextsDir = join12(targetDir, rootDir, "contexts");
|
|
29573
29687
|
if (!await fileExists(contextsDir))
|
|
29574
29688
|
return [];
|
|
29575
29689
|
try {
|
|
@@ -29962,22 +30076,22 @@ async function securityCommand(_options = {}) {
|
|
|
29962
30076
|
|
|
29963
30077
|
// src/cli/serve-commands.ts
|
|
29964
30078
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
29965
|
-
import { join as
|
|
30079
|
+
import { join as join14 } from "node:path";
|
|
29966
30080
|
|
|
29967
30081
|
// src/cli/serve.ts
|
|
29968
30082
|
import { spawn } from "node:child_process";
|
|
29969
|
-
import { existsSync as
|
|
30083
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
29970
30084
|
import { readFile as readFile2, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
29971
|
-
import { join as
|
|
30085
|
+
import { join as join13 } from "node:path";
|
|
29972
30086
|
var DEFAULT_PORT = 4321;
|
|
29973
|
-
var PID_FILE =
|
|
30087
|
+
var PID_FILE = join13(process.env.HOME ?? "~", ".omcustom-serve.pid");
|
|
29974
30088
|
function findServeBuildDir(projectRoot, options) {
|
|
29975
|
-
const localBuild =
|
|
29976
|
-
if (
|
|
30089
|
+
const localBuild = join13(projectRoot, "packages", "serve", "build");
|
|
30090
|
+
if (existsSync3(join13(localBuild, "index.js")))
|
|
29977
30091
|
return localBuild;
|
|
29978
30092
|
if (options?.skipNpmFallback !== true) {
|
|
29979
|
-
const npmBuild =
|
|
29980
|
-
if (
|
|
30093
|
+
const npmBuild = join13(import.meta.dirname, "..", "..", "packages", "serve", "build");
|
|
30094
|
+
if (existsSync3(join13(npmBuild, "index.js")))
|
|
29981
30095
|
return npmBuild;
|
|
29982
30096
|
}
|
|
29983
30097
|
return null;
|
|
@@ -30005,7 +30119,7 @@ async function startServeBackground(projectRoot, port = DEFAULT_PORT, buildDirOp
|
|
|
30005
30119
|
if (buildDir === null) {
|
|
30006
30120
|
return;
|
|
30007
30121
|
}
|
|
30008
|
-
const child = spawn("node", [
|
|
30122
|
+
const child = spawn("node", [join13(buildDir, "index.js")], {
|
|
30009
30123
|
env: {
|
|
30010
30124
|
...process.env,
|
|
30011
30125
|
OMCUSTOM_PORT: String(port),
|
|
@@ -30082,7 +30196,7 @@ function runForeground(projectRoot, port, buildDirOpts) {
|
|
|
30082
30196
|
process.exit(1);
|
|
30083
30197
|
}
|
|
30084
30198
|
console.log(`Web UI: http://localhost:${port}`);
|
|
30085
|
-
spawnSync2("node", [
|
|
30199
|
+
spawnSync2("node", [join14(buildDir, "index.js")], {
|
|
30086
30200
|
env: {
|
|
30087
30201
|
...process.env,
|
|
30088
30202
|
OMCUSTOM_PORT: String(port),
|
|
@@ -30094,12 +30208,199 @@ function runForeground(projectRoot, port, buildDirOpts) {
|
|
|
30094
30208
|
});
|
|
30095
30209
|
}
|
|
30096
30210
|
|
|
30211
|
+
// src/cli/sync.ts
|
|
30212
|
+
import { resolve as resolve2 } from "node:path";
|
|
30213
|
+
|
|
30214
|
+
// src/core/sync.ts
|
|
30215
|
+
init_fs();
|
|
30216
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
30217
|
+
import { cp as cp2, mkdir } from "node:fs/promises";
|
|
30218
|
+
import { join as join15 } from "node:path";
|
|
30219
|
+
async function loadVersions() {
|
|
30220
|
+
try {
|
|
30221
|
+
const packageRoot = getPackageRoot();
|
|
30222
|
+
const manifest = await readJsonFile(join15(packageRoot, "templates", "manifest.json"));
|
|
30223
|
+
const pkg = await readJsonFile(join15(packageRoot, "package.json"));
|
|
30224
|
+
return { generatorVersion: pkg.version, templateVersion: manifest.version };
|
|
30225
|
+
} catch {
|
|
30226
|
+
return { generatorVersion: "0.0.0", templateVersion: "0.0.0" };
|
|
30227
|
+
}
|
|
30228
|
+
}
|
|
30229
|
+
async function generateCurrentLockfile(targetDir) {
|
|
30230
|
+
try {
|
|
30231
|
+
const { generatorVersion, templateVersion } = await loadVersions();
|
|
30232
|
+
return await generateLockfile(targetDir, generatorVersion, templateVersion);
|
|
30233
|
+
} catch {
|
|
30234
|
+
return null;
|
|
30235
|
+
}
|
|
30236
|
+
}
|
|
30237
|
+
async function syncCheck(targetDir, options) {
|
|
30238
|
+
const empty = {
|
|
30239
|
+
inSync: false,
|
|
30240
|
+
added: [],
|
|
30241
|
+
removed: [],
|
|
30242
|
+
modified: [],
|
|
30243
|
+
unchanged: 0,
|
|
30244
|
+
referenceVersion: null,
|
|
30245
|
+
currentVersion: null,
|
|
30246
|
+
totalTracked: 0
|
|
30247
|
+
};
|
|
30248
|
+
const referenceDir = options?.reference ?? targetDir;
|
|
30249
|
+
const reference = await readLockfile(referenceDir);
|
|
30250
|
+
if (!reference) {
|
|
30251
|
+
return empty;
|
|
30252
|
+
}
|
|
30253
|
+
const current = await generateCurrentLockfile(targetDir);
|
|
30254
|
+
if (!current) {
|
|
30255
|
+
return {
|
|
30256
|
+
...empty,
|
|
30257
|
+
referenceVersion: reference.generatorVersion
|
|
30258
|
+
};
|
|
30259
|
+
}
|
|
30260
|
+
const diff = diffLockfiles(reference, current);
|
|
30261
|
+
return {
|
|
30262
|
+
inSync: diff.added.length === 0 && diff.removed.length === 0 && diff.modified.length === 0,
|
|
30263
|
+
added: diff.added,
|
|
30264
|
+
removed: diff.removed,
|
|
30265
|
+
modified: diff.modified,
|
|
30266
|
+
unchanged: diff.unchanged.length,
|
|
30267
|
+
referenceVersion: reference.generatorVersion,
|
|
30268
|
+
currentVersion: current.generatorVersion,
|
|
30269
|
+
totalTracked: Object.keys(current.files).length
|
|
30270
|
+
};
|
|
30271
|
+
}
|
|
30272
|
+
function isExportable(src) {
|
|
30273
|
+
const normalized = src.replace(/\\/g, "/");
|
|
30274
|
+
const excluded = ["/agent-memory", "/agent-memory-local", "/outputs", "settings.local"];
|
|
30275
|
+
return !excluded.some((segment) => {
|
|
30276
|
+
if (segment.startsWith("/")) {
|
|
30277
|
+
return normalized.includes(`${segment}/`) || normalized.endsWith(segment);
|
|
30278
|
+
}
|
|
30279
|
+
return normalized.includes(segment);
|
|
30280
|
+
});
|
|
30281
|
+
}
|
|
30282
|
+
async function countFiles(dir2) {
|
|
30283
|
+
const { readdir: readdir3, stat: stat3 } = await import("node:fs/promises");
|
|
30284
|
+
async function walk(current) {
|
|
30285
|
+
let total = 0;
|
|
30286
|
+
let entries;
|
|
30287
|
+
try {
|
|
30288
|
+
entries = await readdir3(current);
|
|
30289
|
+
} catch {
|
|
30290
|
+
return 0;
|
|
30291
|
+
}
|
|
30292
|
+
for (const entry of entries) {
|
|
30293
|
+
const full = join15(current, entry);
|
|
30294
|
+
try {
|
|
30295
|
+
const s = await stat3(full);
|
|
30296
|
+
if (s.isDirectory()) {
|
|
30297
|
+
total += await walk(full);
|
|
30298
|
+
} else if (s.isFile()) {
|
|
30299
|
+
total += 1;
|
|
30300
|
+
}
|
|
30301
|
+
} catch {}
|
|
30302
|
+
}
|
|
30303
|
+
return total;
|
|
30304
|
+
}
|
|
30305
|
+
return walk(dir2);
|
|
30306
|
+
}
|
|
30307
|
+
async function exportSnapshot(targetDir, outputPath) {
|
|
30308
|
+
const claudeDir = join15(targetDir, ".claude");
|
|
30309
|
+
const guidesDir = join15(targetDir, "guides");
|
|
30310
|
+
if (!existsSync4(claudeDir)) {
|
|
30311
|
+
return { success: false, exportPath: outputPath, fileCount: 0 };
|
|
30312
|
+
}
|
|
30313
|
+
await mkdir(outputPath, { recursive: true });
|
|
30314
|
+
const destClaude = join15(outputPath, ".claude");
|
|
30315
|
+
await cp2(claudeDir, destClaude, {
|
|
30316
|
+
recursive: true,
|
|
30317
|
+
filter: isExportable
|
|
30318
|
+
});
|
|
30319
|
+
if (existsSync4(guidesDir)) {
|
|
30320
|
+
await cp2(guidesDir, join15(outputPath, "guides"), { recursive: true });
|
|
30321
|
+
}
|
|
30322
|
+
const lockfile = await generateCurrentLockfile(targetDir);
|
|
30323
|
+
if (lockfile) {
|
|
30324
|
+
await writeLockfile(outputPath, lockfile);
|
|
30325
|
+
}
|
|
30326
|
+
const fileCount = await countFiles(outputPath);
|
|
30327
|
+
return { success: true, exportPath: outputPath, fileCount };
|
|
30328
|
+
}
|
|
30329
|
+
|
|
30330
|
+
// src/cli/sync.ts
|
|
30331
|
+
async function runExport(targetDir, outputPath) {
|
|
30332
|
+
const result = await exportSnapshot(targetDir, resolve2(outputPath));
|
|
30333
|
+
if (!result.success) {
|
|
30334
|
+
console.error(`
|
|
30335
|
+
Export failed — no .claude/ directory found in current project.`);
|
|
30336
|
+
process.exit(1);
|
|
30337
|
+
}
|
|
30338
|
+
console.log(`
|
|
30339
|
+
Snapshot exported: ${result.exportPath} (${result.fileCount} files)`);
|
|
30340
|
+
console.log(`Team members can install with: omcustom init --from-snapshot ${result.exportPath}`);
|
|
30341
|
+
}
|
|
30342
|
+
function printDriftDetails(result) {
|
|
30343
|
+
if (result.unchanged > 0) {
|
|
30344
|
+
console.log(` ✓ ${result.unchanged} files in sync`);
|
|
30345
|
+
}
|
|
30346
|
+
if (result.modified.length > 0) {
|
|
30347
|
+
console.log(` ⚠ ${result.modified.length} files modified since install:`);
|
|
30348
|
+
for (const f of result.modified) {
|
|
30349
|
+
console.log(` modified: ${f}`);
|
|
30350
|
+
}
|
|
30351
|
+
}
|
|
30352
|
+
if (result.removed.length > 0) {
|
|
30353
|
+
console.log(` ✗ ${result.removed.length} files removed:`);
|
|
30354
|
+
for (const f of result.removed) {
|
|
30355
|
+
console.log(` removed: ${f}`);
|
|
30356
|
+
}
|
|
30357
|
+
}
|
|
30358
|
+
if (result.added.length > 0) {
|
|
30359
|
+
console.log(` + ${result.added.length} files added (not in lockfile):`);
|
|
30360
|
+
for (const f of result.added) {
|
|
30361
|
+
console.log(` added: ${f}`);
|
|
30362
|
+
}
|
|
30363
|
+
}
|
|
30364
|
+
}
|
|
30365
|
+
async function runCheck(targetDir, options) {
|
|
30366
|
+
const result = await syncCheck(targetDir, { reference: options.reference });
|
|
30367
|
+
if (!result.referenceVersion) {
|
|
30368
|
+
console.error(`
|
|
30369
|
+
No lockfile found. Run omcustom init first.`);
|
|
30370
|
+
process.exit(1);
|
|
30371
|
+
}
|
|
30372
|
+
const label = options.reference ? `external snapshot at ${options.reference}` : `lockfile (v${result.referenceVersion})`;
|
|
30373
|
+
console.log(`
|
|
30374
|
+
Sync check — comparing against ${label}
|
|
30375
|
+
`);
|
|
30376
|
+
if (result.inSync) {
|
|
30377
|
+
console.log(` ✓ ${result.unchanged} files in sync`);
|
|
30378
|
+
} else {
|
|
30379
|
+
printDriftDetails(result);
|
|
30380
|
+
}
|
|
30381
|
+
console.log(`
|
|
30382
|
+
Summary: ${result.unchanged} unchanged, ${result.modified.length} modified, ${result.removed.length} removed, ${result.added.length} added`);
|
|
30383
|
+
if (!result.inSync) {
|
|
30384
|
+
process.exit(1);
|
|
30385
|
+
}
|
|
30386
|
+
}
|
|
30387
|
+
function syncCommand(program2) {
|
|
30388
|
+
program2.command("sync").description(i18n.t("cli.sync.description")).option("--check", "Compare current state against lockfile (default behavior)").option("--reference <path>", "Compare against an external snapshot instead of the lockfile").option("--export <path>", "Export current .claude/ state as a reusable snapshot").action(async (options) => {
|
|
30389
|
+
const targetDir = resolve2(".");
|
|
30390
|
+
if (options.export) {
|
|
30391
|
+
await runExport(targetDir, options.export);
|
|
30392
|
+
return;
|
|
30393
|
+
}
|
|
30394
|
+
await runCheck(targetDir, options);
|
|
30395
|
+
});
|
|
30396
|
+
}
|
|
30397
|
+
|
|
30097
30398
|
// src/cli/update.ts
|
|
30098
30399
|
init_package();
|
|
30099
30400
|
|
|
30100
30401
|
// src/core/updater.ts
|
|
30101
30402
|
init_package();
|
|
30102
|
-
import { join as
|
|
30403
|
+
import { join as join16 } from "node:path";
|
|
30103
30404
|
init_fs();
|
|
30104
30405
|
|
|
30105
30406
|
// src/core/entry-merger.ts
|
|
@@ -30354,7 +30655,7 @@ function resolveCustomizations(customizations, configPreserveFiles, targetDir) {
|
|
|
30354
30655
|
}
|
|
30355
30656
|
async function updateEntryDoc(targetDir, config, options) {
|
|
30356
30657
|
const layout = getProviderLayout();
|
|
30357
|
-
const entryPath =
|
|
30658
|
+
const entryPath = join16(targetDir, layout.entryFile);
|
|
30358
30659
|
const templateName = getEntryTemplateName2(config.language);
|
|
30359
30660
|
const templatePath = resolveTemplatePath(templateName);
|
|
30360
30661
|
if (!await fileExists(templatePath)) {
|
|
@@ -30455,7 +30756,7 @@ async function update(options) {
|
|
|
30455
30756
|
result.error = `Downgrade prevented: project has v${result.previousVersion} but CLI is v${cliVersion}. Update the CLI first: npm install -g oh-my-customcode@latest`;
|
|
30456
30757
|
return result;
|
|
30457
30758
|
}
|
|
30458
|
-
const targetPkgPath =
|
|
30759
|
+
const targetPkgPath = join16(options.targetDir, "package.json");
|
|
30459
30760
|
if (await fileExists(targetPkgPath)) {
|
|
30460
30761
|
const targetPkg = await readJsonFile(targetPkgPath);
|
|
30461
30762
|
if (targetPkg.name === "oh-my-customcode") {
|
|
@@ -30571,11 +30872,11 @@ async function collectProtectedSkipPaths(srcPath, destPath, componentPath, force
|
|
|
30571
30872
|
const warnedPaths = [];
|
|
30572
30873
|
const updatedPaths = [];
|
|
30573
30874
|
for (const p of protectedRelative) {
|
|
30574
|
-
const targetFilePath =
|
|
30875
|
+
const targetFilePath = join16(targetDir, componentPath, p);
|
|
30575
30876
|
const lockfileKey = `${componentPath}/${p}`.replace(/\\/g, "/");
|
|
30576
30877
|
const shouldSkip = await shouldSkipProtectedFile(targetFilePath, lockfileKey, lockfile);
|
|
30577
30878
|
if (shouldSkip) {
|
|
30578
|
-
skipPaths.push(path3.relative(destPath,
|
|
30879
|
+
skipPaths.push(path3.relative(destPath, join16(destPath, p)));
|
|
30579
30880
|
warnedPaths.push(p);
|
|
30580
30881
|
} else {
|
|
30581
30882
|
updatedPaths.push(p);
|
|
@@ -30621,7 +30922,7 @@ async function updateComponent(targetDir, component, customizations, options, co
|
|
|
30621
30922
|
const preservedFiles = [];
|
|
30622
30923
|
const componentPath = getComponentPath2(component);
|
|
30623
30924
|
const srcPath = resolveTemplatePath(componentPath);
|
|
30624
|
-
const destPath =
|
|
30925
|
+
const destPath = join16(targetDir, componentPath);
|
|
30625
30926
|
const customComponents = config.customComponents || [];
|
|
30626
30927
|
const skipPaths = [];
|
|
30627
30928
|
if (customizations && !options.forceOverwriteAll) {
|
|
@@ -30663,7 +30964,7 @@ async function updateComponent(targetDir, component, customizations, options, co
|
|
|
30663
30964
|
}
|
|
30664
30965
|
skipPaths.push(...protectedSkipPaths);
|
|
30665
30966
|
const path3 = await import("node:path");
|
|
30666
|
-
const normalizedSkipPaths = skipPaths.map((p) => path3.relative(destPath,
|
|
30967
|
+
const normalizedSkipPaths = skipPaths.map((p) => path3.relative(destPath, join16(targetDir, p)));
|
|
30667
30968
|
const uniqueSkipPaths = [...new Set(normalizedSkipPaths)];
|
|
30668
30969
|
await copyDirectory(srcPath, destPath, {
|
|
30669
30970
|
overwrite: true,
|
|
@@ -30685,12 +30986,12 @@ async function syncRootLevelFiles(targetDir, options) {
|
|
|
30685
30986
|
const layout = getProviderLayout();
|
|
30686
30987
|
const synced = [];
|
|
30687
30988
|
for (const fileName of ROOT_LEVEL_FILES) {
|
|
30688
|
-
const srcPath = resolveTemplatePath(
|
|
30989
|
+
const srcPath = resolveTemplatePath(join16(layout.rootDir, fileName));
|
|
30689
30990
|
if (!await fileExists(srcPath)) {
|
|
30690
30991
|
continue;
|
|
30691
30992
|
}
|
|
30692
|
-
const destPath =
|
|
30693
|
-
await ensureDirectory(
|
|
30993
|
+
const destPath = join16(targetDir, layout.rootDir, fileName);
|
|
30994
|
+
await ensureDirectory(join16(destPath, ".."));
|
|
30694
30995
|
await fs3.copyFile(srcPath, destPath);
|
|
30695
30996
|
if (fileName.endsWith(".sh")) {
|
|
30696
30997
|
await fs3.chmod(destPath, 493);
|
|
@@ -30725,7 +31026,7 @@ async function removeDeprecatedFiles(targetDir, options) {
|
|
|
30725
31026
|
});
|
|
30726
31027
|
continue;
|
|
30727
31028
|
}
|
|
30728
|
-
const fullPath =
|
|
31029
|
+
const fullPath = join16(targetDir, entry.path);
|
|
30729
31030
|
if (await fileExists(fullPath)) {
|
|
30730
31031
|
await fs3.unlink(fullPath);
|
|
30731
31032
|
removed.push(entry.path);
|
|
@@ -30766,7 +31067,7 @@ async function syncNamespaceInFile(targetFilePath, upstreamFilePath) {
|
|
|
30766
31067
|
async function processNamespaceSyncEntry(entry, relPath, fullSrcPath, destPath, componentPath, lockfile) {
|
|
30767
31068
|
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
30768
31069
|
return null;
|
|
30769
|
-
const targetFilePath =
|
|
31070
|
+
const targetFilePath = join16(destPath, relPath);
|
|
30770
31071
|
const lockfileKey = `${componentPath}/${relPath}`.replace(/\\/g, "/");
|
|
30771
31072
|
const shouldSkip = await shouldSkipProtectedFile(targetFilePath, lockfileKey, lockfile);
|
|
30772
31073
|
if (shouldSkip)
|
|
@@ -30781,7 +31082,7 @@ async function applyNamespaceSync(targetDir, component, lockfile) {
|
|
|
30781
31082
|
return [];
|
|
30782
31083
|
const componentPath = getComponentPath2(component);
|
|
30783
31084
|
const srcPath = resolveTemplatePath(componentPath);
|
|
30784
|
-
const destPath =
|
|
31085
|
+
const destPath = join16(targetDir, componentPath);
|
|
30785
31086
|
const fs3 = await import("node:fs/promises");
|
|
30786
31087
|
const synced = [];
|
|
30787
31088
|
const queue = [{ dir: srcPath, relDir: "" }];
|
|
@@ -30795,7 +31096,7 @@ async function applyNamespaceSync(targetDir, component, lockfile) {
|
|
|
30795
31096
|
}
|
|
30796
31097
|
for (const entry of entries) {
|
|
30797
31098
|
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
30798
|
-
const fullSrcPath =
|
|
31099
|
+
const fullSrcPath = join16(dir2, entry.name);
|
|
30799
31100
|
if (entry.isDirectory()) {
|
|
30800
31101
|
queue.push({ dir: fullSrcPath, relDir: relPath });
|
|
30801
31102
|
continue;
|
|
@@ -30818,26 +31119,26 @@ function getComponentPath2(component) {
|
|
|
30818
31119
|
}
|
|
30819
31120
|
async function backupInstallation(targetDir) {
|
|
30820
31121
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
30821
|
-
const backupDir =
|
|
31122
|
+
const backupDir = join16(targetDir, `.omcustom-backup-${timestamp}`);
|
|
30822
31123
|
const fs3 = await import("node:fs/promises");
|
|
30823
31124
|
await ensureDirectory(backupDir);
|
|
30824
31125
|
const layout = getProviderLayout();
|
|
30825
31126
|
const dirsToBackup = [layout.rootDir, "guides"];
|
|
30826
31127
|
for (const dir2 of dirsToBackup) {
|
|
30827
|
-
const srcPath =
|
|
31128
|
+
const srcPath = join16(targetDir, dir2);
|
|
30828
31129
|
if (await fileExists(srcPath)) {
|
|
30829
|
-
const destPath =
|
|
31130
|
+
const destPath = join16(backupDir, dir2);
|
|
30830
31131
|
await copyDirectory(srcPath, destPath, { overwrite: true });
|
|
30831
31132
|
}
|
|
30832
31133
|
}
|
|
30833
|
-
const entryPath =
|
|
31134
|
+
const entryPath = join16(targetDir, layout.entryFile);
|
|
30834
31135
|
if (await fileExists(entryPath)) {
|
|
30835
|
-
await fs3.copyFile(entryPath,
|
|
31136
|
+
await fs3.copyFile(entryPath, join16(backupDir, layout.entryFile));
|
|
30836
31137
|
}
|
|
30837
31138
|
return backupDir;
|
|
30838
31139
|
}
|
|
30839
31140
|
async function loadCustomizationManifest(targetDir) {
|
|
30840
|
-
const manifestPath =
|
|
31141
|
+
const manifestPath = join16(targetDir, CUSTOMIZATION_MANIFEST_FILE);
|
|
30841
31142
|
if (await fileExists(manifestPath)) {
|
|
30842
31143
|
return readJsonFile(manifestPath);
|
|
30843
31144
|
}
|
|
@@ -31078,7 +31379,7 @@ var packageJson = require2("../../package.json");
|
|
|
31078
31379
|
function createProgram() {
|
|
31079
31380
|
const program2 = new Command;
|
|
31080
31381
|
program2.name("omcustom").description(i18n.t("cli.description")).version(packageJson.version, "-v, --version", i18n.t("cli.versionOption")).option("--skip-version-check", "Skip CLI version pre-flight check");
|
|
31081
|
-
program2.command("init").description(i18n.t("cli.init.description")).option("-l, --lang <language>", i18n.t("cli.init.langOption")).option("--domain <domain>", "Install only agents/skills for specific domain (backend, frontend, data-engineering, devops)").option("--yes", "Skip interactive wizard, use defaults").action(async (options) => {
|
|
31382
|
+
program2.command("init").description(i18n.t("cli.init.description")).option("-l, --lang <language>", i18n.t("cli.init.langOption")).option("--domain <domain>", "Install only agents/skills for specific domain (backend, frontend, data-engineering, devops)").option("--yes", "Skip interactive wizard, use defaults").option("--from-snapshot <path>", "Install from a pre-configured team snapshot directory").action(async (options) => {
|
|
31082
31383
|
await initCommand(options);
|
|
31083
31384
|
});
|
|
31084
31385
|
program2.command("update").description(i18n.t("cli.update.description")).option("--dry-run", i18n.t("cli.update.dryRunOption")).option("--force", i18n.t("cli.update.forceOption")).option("--force-overwrite-all", i18n.t("cli.update.forceOverwriteAllOption")).option("--hard", i18n.t("cli.update.hardOption")).option("--backup", i18n.t("cli.update.backupOption")).option("--agents", i18n.t("cli.update.agentsOption")).option("--skills", i18n.t("cli.update.skillsOption")).option("--rules", i18n.t("cli.update.rulesOption")).option("--guides", i18n.t("cli.update.guidesOption")).option("--hooks", i18n.t("cli.update.hooksOption")).option("--contexts", i18n.t("cli.update.contextsOption")).option("--all", i18n.t("cli.update.allOption")).action(async (options) => {
|
|
@@ -31090,6 +31391,7 @@ function createProgram() {
|
|
|
31090
31391
|
verbose: options.verbose
|
|
31091
31392
|
});
|
|
31092
31393
|
});
|
|
31394
|
+
syncCommand(program2);
|
|
31093
31395
|
program2.command("doctor").description(i18n.t("cli.doctor.description")).option("--fix", i18n.t("cli.doctor.fixOption")).option("--updates", i18n.t("cli.doctor.updatesOption")).action(async (options) => {
|
|
31094
31396
|
await doctorCommand(options);
|
|
31095
31397
|
});
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: omcustom:analysis
|
|
3
3
|
description: Analyze project and auto-configure agents, skills, rules, and guides
|
|
4
4
|
scope: harness
|
|
5
|
-
argument-hint: "[
|
|
5
|
+
argument-hint: "[target-dir] [--interview]"
|
|
6
6
|
user-invocable: true
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -13,12 +13,52 @@ Scan a project's tech stack, compare against installed agents/skills, and auto-c
|
|
|
13
13
|
## Options
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
--dry-run
|
|
17
|
-
--verbose
|
|
16
|
+
--dry-run Show what would be added without making changes
|
|
17
|
+
--verbose Show detailed detection reasoning
|
|
18
|
+
--interview, -i Run interactive architecture interview before file-based detection
|
|
18
19
|
```
|
|
19
20
|
|
|
20
21
|
## Workflow
|
|
21
22
|
|
|
23
|
+
### Step 0: Architecture Interview (--interview only)
|
|
24
|
+
|
|
25
|
+
When `--interview` flag is provided, conduct an interactive AI interview before file-based detection. This captures human context that file scanning cannot determine.
|
|
26
|
+
|
|
27
|
+
**Interview flow** (sequential, AI-guided):
|
|
28
|
+
|
|
29
|
+
1. **프로젝트 유형**: "이 프로젝트는 어떤 종류입니까?"
|
|
30
|
+
→ 옵션: web app, REST API, CLI tool, library, monorepo, data pipeline, mobile app
|
|
31
|
+
|
|
32
|
+
2. **아키텍처 패턴**: "어떤 아키텍처를 따르고 있습니까?"
|
|
33
|
+
→ 옵션: microservices, monolith, serverless, event-driven, layered, hexagonal
|
|
34
|
+
|
|
35
|
+
3. **주요 언어**: "주로 사용하는 프로그래밍 언어는?"
|
|
36
|
+
→ 자유 입력, 알려진 에이전트와 매칭
|
|
37
|
+
|
|
38
|
+
4. **배포 대상**: "어디에 배포합니까?"
|
|
39
|
+
→ 옵션: AWS, GCP, Azure, Vercel, on-premises, Docker/K8s, edge
|
|
40
|
+
|
|
41
|
+
5. **팀 우선순위**: "팀의 주요 관심사는?"
|
|
42
|
+
→ 옵션: performance, security, developer experience, cost, scalability
|
|
43
|
+
|
|
44
|
+
**Interview results feed into Step 1 as weighted detection hints:**
|
|
45
|
+
- File evidence + interview agreement = `confidence: high`
|
|
46
|
+
- File evidence only = `confidence: medium` (unchanged from current)
|
|
47
|
+
- Interview only (no file evidence) = `confidence: suggested`
|
|
48
|
+
|
|
49
|
+
**Integration with report:**
|
|
50
|
+
```
|
|
51
|
+
Interview Insights (--interview):
|
|
52
|
+
Project type: REST API (user-specified, confirmed by file scan)
|
|
53
|
+
Architecture: microservices (user-specified)
|
|
54
|
+
Deployment: AWS + Docker (confirmed by file scan)
|
|
55
|
+
Team focus: security → sec-codeql-expert [suggested]
|
|
56
|
+
|
|
57
|
+
Suggested (from interview, no file evidence):
|
|
58
|
+
~ sec-codeql-expert [suggested — no CodeQL config found]
|
|
59
|
+
~ de-kafka-expert [suggested — no kafka deps found]
|
|
60
|
+
```
|
|
61
|
+
|
|
22
62
|
### Step 1: Project Scan
|
|
23
63
|
|
|
24
64
|
Detect tech stack by checking indicator files and dependency manifests.
|
package/templates/manifest.json
CHANGED
|
@@ -7,16 +7,18 @@ mode: auto
|
|
|
7
7
|
error: halt-and-report
|
|
8
8
|
|
|
9
9
|
steps:
|
|
10
|
-
- name:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
- name: issue-analysis
|
|
11
|
+
parallel:
|
|
12
|
+
- name: pre-triage
|
|
13
|
+
skill: professor-triage
|
|
14
|
+
description: Run professor-triage on open issues that lack verify-done label
|
|
15
|
+
condition: "open issues without label:verify-done exist"
|
|
16
|
+
- name: triage
|
|
17
|
+
skill: professor-triage
|
|
18
|
+
description: Analyze verify-done issues against current codebase and perform automated triage
|
|
18
19
|
|
|
19
20
|
- name: plan
|
|
21
|
+
depends_on: issue-analysis
|
|
20
22
|
skill: release-plan
|
|
21
23
|
description: Group triaged issues into release units by priority and size
|
|
22
24
|
input: triage-results
|