oh-my-customcode 1.1.102 → 1.1.104
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/dist/cli/index.js +449 -40
- package/dist/index.js +95 -5
- package/package.json +1 -1
- package/templates/.claude/output-styles/korean-engineer.md +1 -1
- package/templates/.claude/rules/MUST-agent-design.md +2 -2
- package/templates/.claude/rules/MUST-agent-teams.md +18 -7
- package/templates/.claude/rules/MUST-orchestrator-coordination.md +46 -29
- package/templates/.claude/rules/MUST-permissions.md +15 -10
- package/templates/.claude/settings.json +0 -3
- package/templates/.claude/skills/action-validator/SKILL.md +2 -2
- package/templates/.claude/skills/adaptive-harness/SKILL.md +2 -2
- package/templates/.claude/skills/adversarial-review/SKILL.md +1 -1
- package/templates/.claude/skills/agent-eval-framework/SKILL.md +2 -2
- package/templates/.claude/skills/de-lead-routing/SKILL.md +2 -2
- package/templates/.claude/skills/deep-plan/SKILL.md +4 -4
- package/templates/.claude/skills/deep-verify/SKILL.md +3 -3
- package/templates/.claude/skills/design-shotgun/SKILL.md +1 -1
- package/templates/.claude/skills/dev-lead-routing/SKILL.md +2 -2
- package/templates/.claude/skills/dev-review/SKILL.md +2 -2
- package/templates/.claude/skills/evaluator-optimizer/SKILL.md +1 -1
- package/templates/.claude/skills/fsd/SKILL.md +48 -10
- package/templates/.claude/skills/harness-eval/SKILL.md +1 -1
- package/templates/.claude/skills/harness-synthesizer/SKILL.md +1 -1
- package/templates/.claude/skills/homework/SKILL.md +3 -3
- package/templates/.claude/skills/idea/SKILL.md +4 -2
- package/templates/.claude/skills/instinct-extractor/SKILL.md +8 -8
- package/templates/.claude/skills/intent-detection/SKILL.md +1 -1
- package/templates/.claude/skills/multi-model-verification/SKILL.md +2 -2
- package/templates/.claude/skills/omcustom-release-notes/SKILL.md +1 -1
- package/templates/.claude/skills/pipeline/SKILL.md +1 -1
- package/templates/.claude/skills/pipeline/scripts/trusted-issue-filter.jq +37 -0
- package/templates/.claude/skills/pipeline/workflows/auto-dev.yaml +128 -38
- package/templates/.claude/skills/post-release-followup/SKILL.md +2 -2
- package/templates/.claude/skills/professor-triage/SKILL.md +10 -10
- package/templates/.claude/skills/profile/SKILL.md +4 -4
- package/templates/.claude/skills/qa-lead-routing/SKILL.md +2 -2
- package/templates/.claude/skills/release-plan/SKILL.md +1 -1
- package/templates/.claude/skills/research/SKILL.md +8 -6
- package/templates/.claude/skills/result-aggregation/SKILL.md +1 -1
- package/templates/.claude/skills/roundtable-debate/SKILL.md +2 -2
- package/templates/.claude/skills/scout/SKILL.md +3 -8
- package/templates/.claude/skills/sec-agentshield-wrapper/SKILL.md +1 -1
- package/templates/.claude/skills/secretary-routing/SKILL.md +2 -2
- package/templates/.claude/skills/skill-extractor/SKILL.md +1 -1
- package/templates/.claude/skills/structured-dev-cycle/SKILL.md +1 -1
- package/templates/.claude/skills/task-decomposition/SKILL.md +1 -1
- package/templates/.claude/skills/worker-reviewer-pipeline/SKILL.md +5 -1
- package/templates/guides/claude-code/15-version-compatibility.md +20 -4
- package/templates/guides/deep-plan/phases.md +11 -9
- package/templates/guides/harness-engineering/README.md +2 -1
- package/templates/guides/professor-triage/README.md +1 -1
- package/templates/guides/professor-triage/phases.md +1 -1
- package/templates/manifest.json +1 -1
- package/templates/workflows/auto-dev.yaml +128 -38
package/dist/cli/index.js
CHANGED
|
@@ -335,7 +335,7 @@ var init_package = __esm(() => {
|
|
|
335
335
|
workspaces: [
|
|
336
336
|
"packages/*"
|
|
337
337
|
],
|
|
338
|
-
version: "1.1.
|
|
338
|
+
version: "1.1.104",
|
|
339
339
|
description: "Batteries-included agent harness for Claude Code",
|
|
340
340
|
type: "module",
|
|
341
341
|
bin: {
|
|
@@ -14666,12 +14666,16 @@ var en_default = {
|
|
|
14666
14666
|
started: "Web UI started: http://localhost:{{port}}",
|
|
14667
14667
|
failed: "Failed to start Web UI server",
|
|
14668
14668
|
homeUnresolved: "Cannot start the Web UI server: the home directory cannot be resolved, either because HOME is a relative path or because HOME, the OS-reported home and the passwd entry are all empty, so the PID file location cannot be determined. Set HOME to an absolute path.",
|
|
14669
|
-
pidNotWritable: "Cannot start the Web UI server: the PID file {{path}} cannot be written ({{error}}). Check the PID file path and its parent directory."
|
|
14669
|
+
pidNotWritable: "Cannot start the Web UI server: the PID file {{path}} cannot be written ({{error}}). Check the PID file path and its parent directory.",
|
|
14670
|
+
alreadyRunning: "Web UI server is already running (PID {{pid}}); no new server was started. Its port is not recorded — check the port you started it with.",
|
|
14671
|
+
alreadyRunningNotPermitted: "The PID file records PID {{pid}}, which belongs to another user, so this command cannot tell whether it is the Web UI server; no new server was started. If no omcustom Web UI server is running, delete the PID file ({{path}}) and try again.",
|
|
14672
|
+
startingElsewhere: "Another Web UI start is in progress; this command did not start a server."
|
|
14670
14673
|
},
|
|
14671
14674
|
stop: {
|
|
14672
14675
|
description: "Stop the Web UI server",
|
|
14673
14676
|
stopped: "Web UI server stopped",
|
|
14674
|
-
notRunning: "Web UI server is not running"
|
|
14677
|
+
notRunning: "Web UI server is not running",
|
|
14678
|
+
notPermitted: "Cannot stop the Web UI server: process {{pid}} recorded in {{path}} exists but you are not permitted to signal it. It belongs to another user, or the PID file is stale and the PID was reused by an unrelated process. Stop it as that user, or remove the PID file if it is stale."
|
|
14675
14679
|
},
|
|
14676
14680
|
status: {
|
|
14677
14681
|
description: "Show Web UI server status",
|
|
@@ -15086,12 +15090,16 @@ var ko_default = {
|
|
|
15086
15090
|
started: "Web UI 시작됨: http://localhost:{{port}}",
|
|
15087
15091
|
failed: "Web UI 서버 시작 실패",
|
|
15088
15092
|
homeUnresolved: "Web UI 서버를 시작할 수 없습니다: 홈 디렉토리를 확인할 수 없어 PID 파일 위치를 정할 수 없습니다. HOME이 상대 경로이거나, HOME·OS 보고 홈·passwd 항목이 모두 비어 있는 경우입니다. HOME을 절대 경로로 설정하세요.",
|
|
15089
|
-
pidNotWritable: "Web UI 서버를 시작할 수 없습니다: PID 파일 {{path}}에 쓸 수 없습니다 ({{error}}). PID 파일 경로와 그 상위 디렉토리를 확인하세요."
|
|
15093
|
+
pidNotWritable: "Web UI 서버를 시작할 수 없습니다: PID 파일 {{path}}에 쓸 수 없습니다 ({{error}}). PID 파일 경로와 그 상위 디렉토리를 확인하세요.",
|
|
15094
|
+
alreadyRunning: "Web UI 서버가 이미 실행 중입니다 (PID {{pid}}). 새 서버를 시작하지 않았습니다. 서버의 포트는 기록되지 않으므로 시작할 때 지정한 포트를 확인하세요.",
|
|
15095
|
+
alreadyRunningNotPermitted: "PID 파일에 기록된 PID {{pid}}는 다른 사용자의 프로세스라서 Web UI 서버인지 이 명령으로 확인할 수 없습니다. 새 서버를 시작하지 않았습니다. omcustom Web UI 서버가 실행 중이 아니라면 PID 파일({{path}})을 삭제한 뒤 다시 시도하세요.",
|
|
15096
|
+
startingElsewhere: "다른 Web UI 시작이 진행 중입니다. 이 명령은 서버를 시작하지 않았습니다."
|
|
15090
15097
|
},
|
|
15091
15098
|
stop: {
|
|
15092
15099
|
description: "Web UI 서버 중지",
|
|
15093
15100
|
stopped: "Web UI 서버가 중지되었습니다",
|
|
15094
|
-
notRunning: "Web UI 서버가 실행 중이 아닙니다"
|
|
15101
|
+
notRunning: "Web UI 서버가 실행 중이 아닙니다",
|
|
15102
|
+
notPermitted: "Web UI 서버를 중지할 수 없습니다: {{path}}에 기록된 프로세스 {{pid}}가 존재하지만 신호를 보낼 권한이 없습니다. 다른 사용자의 프로세스이거나, PID 파일이 오래되어 PID가 무관한 프로세스에 재사용된 경우입니다. 해당 사용자로 중지하거나, 오래된 PID 파일이면 삭제하세요."
|
|
15095
15103
|
},
|
|
15096
15104
|
status: {
|
|
15097
15105
|
description: "Web UI 서버 상태 표시",
|
|
@@ -15786,6 +15794,7 @@ var MESSAGES = {
|
|
|
15786
15794
|
"lockfile.component_dir_missing": "Component directory missing: {{path}}",
|
|
15787
15795
|
"lockfile.hash_failed": "Failed to hash file: {{path}} — {{error}}",
|
|
15788
15796
|
"lockfile.entry_added": "Lockfile entry added: {{path}} ({{component}})",
|
|
15797
|
+
"lockfile.entry_git_excluded": "Lockfile entry skipped (not in git listing): {{path}}",
|
|
15789
15798
|
"update.start": "Checking for updates...",
|
|
15790
15799
|
"update.success": "Updated from {{from}} to {{to}}",
|
|
15791
15800
|
"update.components_synced": "Components synced (version {{version}}): {{components}}",
|
|
@@ -15907,6 +15916,7 @@ var MESSAGES = {
|
|
|
15907
15916
|
"lockfile.component_dir_missing": "컴포넌트 디렉토리 없음: {{path}}",
|
|
15908
15917
|
"lockfile.hash_failed": "파일 해시 실패: {{path}} — {{error}}",
|
|
15909
15918
|
"lockfile.entry_added": "잠금 파일 항목 추가: {{path}} ({{component}})",
|
|
15919
|
+
"lockfile.entry_git_excluded": "잠금 파일 항목 제외 (git 목록에 없음): {{path}}",
|
|
15910
15920
|
"update.start": "업데이트 확인 중...",
|
|
15911
15921
|
"update.success": "{{from}}에서 {{to}}로 업데이트 완료",
|
|
15912
15922
|
"update.components_synced": "컴포넌트 동기화 완료 (버전 {{version}}): {{components}}",
|
|
@@ -16362,10 +16372,12 @@ function getComponentPath(component) {
|
|
|
16362
16372
|
|
|
16363
16373
|
// src/core/lockfile.ts
|
|
16364
16374
|
init_fs();
|
|
16375
|
+
import { execFile } from "node:child_process";
|
|
16365
16376
|
import { createHash } from "node:crypto";
|
|
16366
16377
|
import { createReadStream } from "node:fs";
|
|
16367
|
-
import { readdir, stat } from "node:fs/promises";
|
|
16378
|
+
import { readdir, realpath, stat } from "node:fs/promises";
|
|
16368
16379
|
import { join as join7, relative as relative2 } from "node:path";
|
|
16380
|
+
import { promisify } from "node:util";
|
|
16369
16381
|
var LOCKFILE_NAME = ".omcustom.lock.json";
|
|
16370
16382
|
var LOCKFILE_VERSION = 1;
|
|
16371
16383
|
var LOCKFILE_COMPONENTS = [
|
|
@@ -16378,6 +16390,87 @@ var LOCKFILE_COMPONENTS = [
|
|
|
16378
16390
|
"guides"
|
|
16379
16391
|
];
|
|
16380
16392
|
var COMPONENT_PATHS = LOCKFILE_COMPONENTS.map((component) => [getComponentPath(component), component]);
|
|
16393
|
+
var execFileAsync = promisify(execFile);
|
|
16394
|
+
var GIT_LOCATION_ENV_KEYS = [
|
|
16395
|
+
"GIT_DIR",
|
|
16396
|
+
"GIT_WORK_TREE",
|
|
16397
|
+
"GIT_INDEX_FILE",
|
|
16398
|
+
"GIT_PREFIX",
|
|
16399
|
+
"GIT_COMMON_DIR"
|
|
16400
|
+
];
|
|
16401
|
+
var GIT_INJECTED_CONFIG_ENV_KEYS = ["GIT_CONFIG_PARAMETERS", "GIT_CONFIG_COUNT"];
|
|
16402
|
+
var GIT_INJECTED_CONFIG_ENV_PREFIXES = ["GIT_CONFIG_KEY_", "GIT_CONFIG_VALUE_"];
|
|
16403
|
+
var GIT_REQUIRED_HINT = "build from a git checkout: excluding ignored local files from the lockfile requires git (#1819)";
|
|
16404
|
+
function gitChildEnv() {
|
|
16405
|
+
const env = { ...process.env };
|
|
16406
|
+
for (const key of [...GIT_LOCATION_ENV_KEYS, ...GIT_INJECTED_CONFIG_ENV_KEYS]) {
|
|
16407
|
+
delete env[key];
|
|
16408
|
+
}
|
|
16409
|
+
for (const key of Object.keys(env)) {
|
|
16410
|
+
if (GIT_INJECTED_CONFIG_ENV_PREFIXES.some((prefix) => key.startsWith(prefix))) {
|
|
16411
|
+
delete env[key];
|
|
16412
|
+
}
|
|
16413
|
+
}
|
|
16414
|
+
return env;
|
|
16415
|
+
}
|
|
16416
|
+
async function runGit(targetDir, args, env) {
|
|
16417
|
+
try {
|
|
16418
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
16419
|
+
cwd: targetDir,
|
|
16420
|
+
env,
|
|
16421
|
+
encoding: "utf-8",
|
|
16422
|
+
maxBuffer: 256 * 1024 * 1024
|
|
16423
|
+
});
|
|
16424
|
+
return stdout;
|
|
16425
|
+
} catch (err) {
|
|
16426
|
+
const detail = err instanceof Error ? err.message.trim() : String(err);
|
|
16427
|
+
throw new Error(`git ${args[0]} failed in ${targetDir}: ${detail} — ${GIT_REQUIRED_HINT}`);
|
|
16428
|
+
}
|
|
16429
|
+
}
|
|
16430
|
+
function foldCase(nfcPath) {
|
|
16431
|
+
return nfcPath.toLowerCase();
|
|
16432
|
+
}
|
|
16433
|
+
function isGitVisible(visible, relativePath) {
|
|
16434
|
+
if (visible.exact.has(relativePath)) {
|
|
16435
|
+
return true;
|
|
16436
|
+
}
|
|
16437
|
+
const nfc = relativePath.normalize("NFC");
|
|
16438
|
+
if (visible.nfc.has(nfc)) {
|
|
16439
|
+
return true;
|
|
16440
|
+
}
|
|
16441
|
+
return visible.folded?.has(foldCase(nfc)) ?? false;
|
|
16442
|
+
}
|
|
16443
|
+
function stripFinalNewline(output) {
|
|
16444
|
+
return output.endsWith(`
|
|
16445
|
+
`) ? output.slice(0, -1) : output;
|
|
16446
|
+
}
|
|
16447
|
+
async function assertGitWorkTreeRoot(targetDir, env) {
|
|
16448
|
+
const topLevel = stripFinalNewline(await runGit(targetDir, ["rev-parse", "--show-toplevel"], env));
|
|
16449
|
+
let realTop;
|
|
16450
|
+
let realTarget;
|
|
16451
|
+
try {
|
|
16452
|
+
[realTop, realTarget] = await Promise.all([realpath(topLevel), realpath(targetDir)]);
|
|
16453
|
+
} catch (err) {
|
|
16454
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
16455
|
+
throw new Error(`cannot resolve the git work tree root of ${targetDir}: ${detail} — ${GIT_REQUIRED_HINT}`);
|
|
16456
|
+
}
|
|
16457
|
+
if (realTop !== realTarget) {
|
|
16458
|
+
throw new Error(`${targetDir} is not the root of its git work tree (root: ${topLevel}) — ${GIT_REQUIRED_HINT}`);
|
|
16459
|
+
}
|
|
16460
|
+
}
|
|
16461
|
+
async function listGitVisiblePaths(targetDir) {
|
|
16462
|
+
const env = gitChildEnv();
|
|
16463
|
+
await assertGitWorkTreeRoot(targetDir, env);
|
|
16464
|
+
const pathspecs = COMPONENT_PATHS.map(([prefix]) => prefix);
|
|
16465
|
+
const [listing, ignoreCase] = await Promise.all([
|
|
16466
|
+
runGit(targetDir, ["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", ...pathspecs], env),
|
|
16467
|
+
runGit(targetDir, ["config", "--type=bool", "--default=false", "--get", "core.ignorecase"], env)
|
|
16468
|
+
]);
|
|
16469
|
+
const exact = new Set(listing.split("\x00").filter((entry) => entry.length > 0));
|
|
16470
|
+
const nfc = new Set([...exact].map((entry) => entry.normalize("NFC")));
|
|
16471
|
+
const folded = stripFinalNewline(ignoreCase) === "true" ? new Set([...nfc].map(foldCase)) : null;
|
|
16472
|
+
return { exact, nfc, folded };
|
|
16473
|
+
}
|
|
16381
16474
|
function computeFileHash(filePath) {
|
|
16382
16475
|
return new Promise((resolve, reject) => {
|
|
16383
16476
|
const hash = createHash("sha256");
|
|
@@ -16463,8 +16556,9 @@ function sortFilesByPath(files) {
|
|
|
16463
16556
|
const sorted = Object.keys(files).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
16464
16557
|
return Object.fromEntries(sorted.map((key) => [key, files[key]]));
|
|
16465
16558
|
}
|
|
16466
|
-
async function generateLockfile(targetDir, generatorVersion, templateVersion) {
|
|
16559
|
+
async function generateLockfile(targetDir, generatorVersion, templateVersion, options = {}) {
|
|
16467
16560
|
const files = {};
|
|
16561
|
+
const gitVisible = options.excludeGitIgnored ? await listGitVisiblePaths(targetDir) : null;
|
|
16468
16562
|
const componentRoots = COMPONENT_PATHS.map(([prefix]) => join7(targetDir, prefix));
|
|
16469
16563
|
for (const componentRoot of componentRoots) {
|
|
16470
16564
|
const exists = await fileExists(componentRoot);
|
|
@@ -16475,6 +16569,10 @@ async function generateLockfile(targetDir, generatorVersion, templateVersion) {
|
|
|
16475
16569
|
const allFiles = await collectFiles(componentRoot, targetDir, false);
|
|
16476
16570
|
for (const absolutePath of allFiles) {
|
|
16477
16571
|
const relativePath = relative2(targetDir, absolutePath).replace(/\\/g, "/");
|
|
16572
|
+
if (gitVisible !== null && !isGitVisible(gitVisible, relativePath)) {
|
|
16573
|
+
debug("lockfile.entry_git_excluded", { path: relativePath });
|
|
16574
|
+
continue;
|
|
16575
|
+
}
|
|
16478
16576
|
let hash;
|
|
16479
16577
|
let size;
|
|
16480
16578
|
try {
|
|
@@ -16535,12 +16633,12 @@ function preserveGeneratedAt(next, previous) {
|
|
|
16535
16633
|
}
|
|
16536
16634
|
return { ...next, generatedAt: previous.generatedAt };
|
|
16537
16635
|
}
|
|
16538
|
-
async function generateAndWriteLockfileForDir(targetDir) {
|
|
16636
|
+
async function generateAndWriteLockfileForDir(targetDir, options = {}) {
|
|
16539
16637
|
try {
|
|
16540
16638
|
const packageRoot = getPackageRoot();
|
|
16541
16639
|
const manifest = await readJsonFile(join7(packageRoot, "templates", "manifest.json"));
|
|
16542
16640
|
const { version: generatorVersion } = await readJsonFile(join7(packageRoot, "package.json"));
|
|
16543
|
-
const generated = await generateLockfile(targetDir, generatorVersion, manifest.version);
|
|
16641
|
+
const generated = await generateLockfile(targetDir, generatorVersion, manifest.version, options);
|
|
16544
16642
|
const previous = await readLockfile(targetDir);
|
|
16545
16643
|
const lockfile = preserveGeneratedAt(generated, previous);
|
|
16546
16644
|
await writeLockfile(targetDir, lockfile);
|
|
@@ -20938,11 +21036,31 @@ import { join as join17 } from "node:path";
|
|
|
20938
21036
|
// src/cli/serve.ts
|
|
20939
21037
|
init_home();
|
|
20940
21038
|
import { spawn } from "node:child_process";
|
|
21039
|
+
import { randomUUID } from "node:crypto";
|
|
20941
21040
|
import { constants as constants3, existsSync as existsSync3 } from "node:fs";
|
|
20942
|
-
import {
|
|
21041
|
+
import {
|
|
21042
|
+
access,
|
|
21043
|
+
link,
|
|
21044
|
+
readdir as readdir3,
|
|
21045
|
+
readFile as readFile3,
|
|
21046
|
+
readlink,
|
|
21047
|
+
rename as rename2,
|
|
21048
|
+
stat as stat3,
|
|
21049
|
+
unlink,
|
|
21050
|
+
writeFile as writeFile3
|
|
21051
|
+
} from "node:fs/promises";
|
|
20943
21052
|
import { dirname as dirname7, isAbsolute as isAbsolute4, join as join16 } from "node:path";
|
|
21053
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
20944
21054
|
var DEFAULT_PORT = 4321;
|
|
20945
21055
|
var PID_FILE_NAME = ".omcustom-serve.pid";
|
|
21056
|
+
var STARTING_RECORD = /^starting:(\d+):/;
|
|
21057
|
+
var MAX_PID = 2147483647;
|
|
21058
|
+
var MAX_CLAIM_ATTEMPTS = 3;
|
|
21059
|
+
var CLAIM_MAX_AGE_MS = 60000;
|
|
21060
|
+
var PARTIAL_RECORD_GRACE_MS = 1e4;
|
|
21061
|
+
var ABSENT_RETRY_DELAY_MS = 10;
|
|
21062
|
+
var HARD_LINK_UNSUPPORTED = new Set(["ENOTSUP", "EOPNOTSUPP", "EPERM", "EXDEV"]);
|
|
21063
|
+
var SIBLING_REMNANT = /^\.omcustom-serve\.pid\.(\d+)\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(?:tmp|taken)$/;
|
|
20946
21064
|
|
|
20947
21065
|
class ServePidFileError extends Error {
|
|
20948
21066
|
reason;
|
|
@@ -20954,6 +21072,17 @@ class ServePidFileError extends Error {
|
|
|
20954
21072
|
this.pidFile = pidFile;
|
|
20955
21073
|
}
|
|
20956
21074
|
}
|
|
21075
|
+
|
|
21076
|
+
class ServeStopPermissionError extends Error {
|
|
21077
|
+
pid;
|
|
21078
|
+
pidFile;
|
|
21079
|
+
constructor(pid, pidFile, options) {
|
|
21080
|
+
super(`Not permitted to signal the serve process ${pid} recorded in ${pidFile}`, options);
|
|
21081
|
+
this.name = "ServeStopPermissionError";
|
|
21082
|
+
this.pid = pid;
|
|
21083
|
+
this.pidFile = pidFile;
|
|
21084
|
+
}
|
|
21085
|
+
}
|
|
20957
21086
|
function resolveServePidFile(sources) {
|
|
20958
21087
|
const home = resolveHomeDir(sources);
|
|
20959
21088
|
if (!isAbsolute4(home)) {
|
|
@@ -20966,12 +21095,65 @@ function findServeBuildDir(projectRoot, options) {
|
|
|
20966
21095
|
if (existsSync3(join16(localBuild, "index.js")))
|
|
20967
21096
|
return localBuild;
|
|
20968
21097
|
if (options?.skipNpmFallback !== true) {
|
|
20969
|
-
const
|
|
21098
|
+
const npmPackageRoot = options?.npmPackageRoot ?? join16(import.meta.dirname, "..", "..");
|
|
21099
|
+
const npmBuild = join16(npmPackageRoot, "packages", "serve", "build");
|
|
20970
21100
|
if (existsSync3(join16(npmBuild, "index.js")))
|
|
20971
21101
|
return npmBuild;
|
|
20972
21102
|
}
|
|
20973
21103
|
return null;
|
|
20974
21104
|
}
|
|
21105
|
+
function parsePidRecord(raw) {
|
|
21106
|
+
const text = raw.trim();
|
|
21107
|
+
const match = STARTING_RECORD.exec(text);
|
|
21108
|
+
const pid = Number(match === null ? text : match[1]);
|
|
21109
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || pid > MAX_PID) {
|
|
21110
|
+
return null;
|
|
21111
|
+
}
|
|
21112
|
+
return { pid, starting: match !== null };
|
|
21113
|
+
}
|
|
21114
|
+
function probeProcess(pid) {
|
|
21115
|
+
try {
|
|
21116
|
+
process.kill(pid, 0);
|
|
21117
|
+
return "exists";
|
|
21118
|
+
} catch (error) {
|
|
21119
|
+
const code = errorCode(error);
|
|
21120
|
+
if (code === "ESRCH") {
|
|
21121
|
+
return "gone";
|
|
21122
|
+
}
|
|
21123
|
+
return code === "EPERM" ? "not-permitted" : "unknown";
|
|
21124
|
+
}
|
|
21125
|
+
}
|
|
21126
|
+
function isProcessAlive(pid) {
|
|
21127
|
+
return probeProcess(pid) !== "gone";
|
|
21128
|
+
}
|
|
21129
|
+
function errorCode(error) {
|
|
21130
|
+
return error?.code;
|
|
21131
|
+
}
|
|
21132
|
+
async function isOlderThan(path, ms) {
|
|
21133
|
+
try {
|
|
21134
|
+
const { mtimeMs } = await stat3(path);
|
|
21135
|
+
return Date.now() - mtimeMs >= ms;
|
|
21136
|
+
} catch {
|
|
21137
|
+
return false;
|
|
21138
|
+
}
|
|
21139
|
+
}
|
|
21140
|
+
async function isRecordLive(pidFile, record) {
|
|
21141
|
+
if (!isProcessAlive(record.pid)) {
|
|
21142
|
+
return false;
|
|
21143
|
+
}
|
|
21144
|
+
return !record.starting || !await isOlderThan(pidFile, CLAIM_MAX_AGE_MS);
|
|
21145
|
+
}
|
|
21146
|
+
async function readPidFileSettled(pidFile) {
|
|
21147
|
+
try {
|
|
21148
|
+
return await readFile3(pidFile, "utf-8");
|
|
21149
|
+
} catch (error) {
|
|
21150
|
+
if (errorCode(error) !== "ENOENT") {
|
|
21151
|
+
throw error;
|
|
21152
|
+
}
|
|
21153
|
+
}
|
|
21154
|
+
await delay(ABSENT_RETRY_DELAY_MS);
|
|
21155
|
+
return readFile3(pidFile, "utf-8");
|
|
21156
|
+
}
|
|
20975
21157
|
async function isServeRunning() {
|
|
20976
21158
|
const pidFile = resolveServePidFile();
|
|
20977
21159
|
if (pidFile === null) {
|
|
@@ -20980,37 +21162,64 @@ async function isServeRunning() {
|
|
|
20980
21162
|
return isServeRunningAt(pidFile);
|
|
20981
21163
|
}
|
|
20982
21164
|
async function isServeRunningAt(pidFile) {
|
|
21165
|
+
return await readLiveRecord(pidFile) !== null;
|
|
21166
|
+
}
|
|
21167
|
+
async function readLiveRecord(pidFile) {
|
|
21168
|
+
let raw;
|
|
20983
21169
|
try {
|
|
20984
|
-
|
|
20985
|
-
const pid = Number(raw.trim());
|
|
20986
|
-
if (!Number.isFinite(pid) || pid <= 0) {
|
|
20987
|
-
await cleanupPidFile(pidFile);
|
|
20988
|
-
return false;
|
|
20989
|
-
}
|
|
20990
|
-
process.kill(pid, 0);
|
|
20991
|
-
return true;
|
|
21170
|
+
raw = await readPidFileSettled(pidFile);
|
|
20992
21171
|
} catch {
|
|
20993
|
-
|
|
20994
|
-
|
|
21172
|
+
return null;
|
|
21173
|
+
}
|
|
21174
|
+
const record = parsePidRecord(raw);
|
|
21175
|
+
return record !== null && await isRecordLive(pidFile, record) ? record : null;
|
|
21176
|
+
}
|
|
21177
|
+
function heldResult(record) {
|
|
21178
|
+
if (record === null || record.starting) {
|
|
21179
|
+
return { status: "starting-elsewhere" };
|
|
21180
|
+
}
|
|
21181
|
+
const probe = probeProcess(record.pid);
|
|
21182
|
+
if (probe === "gone") {
|
|
21183
|
+
return { status: "starting-elsewhere" };
|
|
20995
21184
|
}
|
|
21185
|
+
return probe === "not-permitted" ? { status: "already-running", pid: record.pid, notPermitted: true } : { status: "already-running", pid: record.pid };
|
|
21186
|
+
}
|
|
21187
|
+
async function getServerPid() {
|
|
21188
|
+
const pidFile = resolveServePidFile();
|
|
21189
|
+
if (pidFile === null) {
|
|
21190
|
+
return null;
|
|
21191
|
+
}
|
|
21192
|
+
const record = await readLiveRecord(pidFile);
|
|
21193
|
+
return record !== null && !record.starting ? record.pid : null;
|
|
20996
21194
|
}
|
|
20997
21195
|
async function startServeBackground(projectRoot, port = DEFAULT_PORT, buildDirOpts) {
|
|
20998
21196
|
const pidFile = resolveServePidFile();
|
|
20999
21197
|
if (pidFile === null) {
|
|
21000
21198
|
throw new ServePidFileError("home-unresolved", null);
|
|
21001
21199
|
}
|
|
21002
|
-
|
|
21003
|
-
|
|
21200
|
+
const running = await readLiveRecord(pidFile);
|
|
21201
|
+
if (running !== null) {
|
|
21202
|
+
return heldResult(running);
|
|
21004
21203
|
}
|
|
21005
21204
|
const buildDir = findServeBuildDir(projectRoot, buildDirOpts);
|
|
21006
21205
|
if (buildDir === null) {
|
|
21007
|
-
return;
|
|
21206
|
+
return { status: "build-missing" };
|
|
21008
21207
|
}
|
|
21009
21208
|
try {
|
|
21010
21209
|
await access(dirname7(pidFile), constants3.W_OK);
|
|
21011
21210
|
} catch (error) {
|
|
21012
21211
|
throw new ServePidFileError("pid-not-writable", pidFile, { cause: error });
|
|
21013
21212
|
}
|
|
21213
|
+
let claimed;
|
|
21214
|
+
try {
|
|
21215
|
+
claimed = await claimPidFile(pidFile);
|
|
21216
|
+
} catch (error) {
|
|
21217
|
+
throw new ServePidFileError("pid-not-writable", pidFile, { cause: error });
|
|
21218
|
+
}
|
|
21219
|
+
if ("held" in claimed) {
|
|
21220
|
+
return heldResult(claimed.held);
|
|
21221
|
+
}
|
|
21222
|
+
const { claim } = claimed;
|
|
21014
21223
|
const child = spawn("node", [join16(buildDir, "index.js")], {
|
|
21015
21224
|
env: {
|
|
21016
21225
|
...process.env,
|
|
@@ -21025,38 +21234,214 @@ async function startServeBackground(projectRoot, port = DEFAULT_PORT, buildDirOp
|
|
|
21025
21234
|
child.on("error", () => {});
|
|
21026
21235
|
child.unref();
|
|
21027
21236
|
if (child.pid === undefined) {
|
|
21028
|
-
|
|
21237
|
+
await removePidFileIf(pidFile, claim);
|
|
21238
|
+
return { status: "spawn-failed" };
|
|
21029
21239
|
}
|
|
21240
|
+
let outcome;
|
|
21030
21241
|
try {
|
|
21031
|
-
await
|
|
21242
|
+
outcome = await recordServerPid(pidFile, claim, String(child.pid));
|
|
21032
21243
|
} catch (error) {
|
|
21033
21244
|
child.kill("SIGTERM");
|
|
21245
|
+
await removePidFileIf(pidFile, claim);
|
|
21034
21246
|
throw new ServePidFileError("pid-not-writable", pidFile, { cause: error });
|
|
21035
21247
|
}
|
|
21248
|
+
if (!outcome.recorded) {
|
|
21249
|
+
child.kill("SIGTERM");
|
|
21250
|
+
return heldResult(outcome.lost);
|
|
21251
|
+
}
|
|
21252
|
+
return { status: "started", pid: child.pid };
|
|
21253
|
+
}
|
|
21254
|
+
async function recordServerPid(pidFile, claim, pid) {
|
|
21255
|
+
for (let attempt = 0;attempt < MAX_CLAIM_ATTEMPTS; attempt++) {
|
|
21256
|
+
if (await createPidFileExclusive(pidFile, pid)) {
|
|
21257
|
+
return { recorded: true };
|
|
21258
|
+
}
|
|
21259
|
+
let current;
|
|
21260
|
+
try {
|
|
21261
|
+
current = await readFile3(pidFile, "utf-8");
|
|
21262
|
+
} catch (error) {
|
|
21263
|
+
if (errorCode(error) === "ENOENT") {
|
|
21264
|
+
continue;
|
|
21265
|
+
}
|
|
21266
|
+
throw error;
|
|
21267
|
+
}
|
|
21268
|
+
if (current !== claim) {
|
|
21269
|
+
return { recorded: false, lost: parsePidRecord(current) };
|
|
21270
|
+
}
|
|
21271
|
+
await replacePidFile(pidFile, pid);
|
|
21272
|
+
return { recorded: true };
|
|
21273
|
+
}
|
|
21274
|
+
return { recorded: false, lost: null };
|
|
21036
21275
|
}
|
|
21037
21276
|
async function stopServe() {
|
|
21038
21277
|
const pidFile = resolveServePidFile();
|
|
21039
21278
|
if (pidFile === null) {
|
|
21040
21279
|
return false;
|
|
21041
21280
|
}
|
|
21281
|
+
let raw;
|
|
21042
21282
|
try {
|
|
21043
|
-
|
|
21044
|
-
|
|
21045
|
-
|
|
21046
|
-
|
|
21047
|
-
|
|
21283
|
+
raw = await readPidFileSettled(pidFile);
|
|
21284
|
+
} catch {
|
|
21285
|
+
return false;
|
|
21286
|
+
}
|
|
21287
|
+
const record = parsePidRecord(raw);
|
|
21288
|
+
if (record === null || record.starting) {
|
|
21289
|
+
return false;
|
|
21290
|
+
}
|
|
21291
|
+
try {
|
|
21292
|
+
process.kill(record.pid, "SIGTERM");
|
|
21293
|
+
} catch (error) {
|
|
21294
|
+
if (errorCode(error) === "EPERM") {
|
|
21295
|
+
throw new ServeStopPermissionError(record.pid, pidFile, { cause: error });
|
|
21296
|
+
}
|
|
21297
|
+
return false;
|
|
21298
|
+
}
|
|
21299
|
+
await removePidFileIf(pidFile, raw);
|
|
21300
|
+
return true;
|
|
21301
|
+
}
|
|
21302
|
+
function siblingPath(pidFile, suffix) {
|
|
21303
|
+
return `${pidFile}.${process.pid}.${randomUUID()}.${suffix}`;
|
|
21304
|
+
}
|
|
21305
|
+
async function claimPidFile(pidFile) {
|
|
21306
|
+
await removeDeadSiblings(pidFile);
|
|
21307
|
+
const claim = `starting:${process.pid}:${randomUUID()}`;
|
|
21308
|
+
for (let attempt = 0;attempt < MAX_CLAIM_ATTEMPTS; attempt++) {
|
|
21309
|
+
if (await createPidFileExclusive(pidFile, claim)) {
|
|
21310
|
+
return { claim };
|
|
21311
|
+
}
|
|
21312
|
+
let raw;
|
|
21313
|
+
try {
|
|
21314
|
+
raw = await readFile3(pidFile, "utf-8");
|
|
21315
|
+
} catch (error) {
|
|
21316
|
+
if (errorCode(error) === "ENOENT") {
|
|
21317
|
+
await removeDanglingLink(pidFile);
|
|
21318
|
+
continue;
|
|
21319
|
+
}
|
|
21320
|
+
throw error;
|
|
21321
|
+
}
|
|
21322
|
+
if (await isRecordHeld(pidFile, raw)) {
|
|
21323
|
+
return { held: parsePidRecord(raw) };
|
|
21048
21324
|
}
|
|
21049
|
-
|
|
21050
|
-
|
|
21325
|
+
await removePidFileIf(pidFile, raw);
|
|
21326
|
+
}
|
|
21327
|
+
return { held: null };
|
|
21328
|
+
}
|
|
21329
|
+
async function isRecordHeld(pidFile, raw) {
|
|
21330
|
+
const record = parsePidRecord(raw);
|
|
21331
|
+
if (record === null) {
|
|
21332
|
+
return isPossiblyPartialRecord(raw) && !await isOlderThan(pidFile, PARTIAL_RECORD_GRACE_MS);
|
|
21333
|
+
}
|
|
21334
|
+
return isRecordLive(pidFile, record);
|
|
21335
|
+
}
|
|
21336
|
+
function isPossiblyPartialRecord(raw) {
|
|
21337
|
+
return "starting:".startsWith(raw) || /^starting:\d*$/.test(raw);
|
|
21338
|
+
}
|
|
21339
|
+
async function removeDeadSiblings(pidFile) {
|
|
21340
|
+
const dir = dirname7(pidFile);
|
|
21341
|
+
let names;
|
|
21342
|
+
try {
|
|
21343
|
+
names = await readdir3(dir);
|
|
21344
|
+
} catch {
|
|
21345
|
+
return;
|
|
21346
|
+
}
|
|
21347
|
+
for (const name of names) {
|
|
21348
|
+
const match = SIBLING_REMNANT.exec(name);
|
|
21349
|
+
if (match !== null && !isProcessAlive(Number(match[1]))) {
|
|
21350
|
+
await unlinkQuietly(join16(dir, name));
|
|
21351
|
+
}
|
|
21352
|
+
}
|
|
21353
|
+
}
|
|
21354
|
+
async function createPidFileExclusive(pidFile, content) {
|
|
21355
|
+
const staged = siblingPath(pidFile, "tmp");
|
|
21356
|
+
try {
|
|
21357
|
+
await writeFile3(staged, content, { encoding: "utf-8", flag: "wx" });
|
|
21358
|
+
await linkExclusive(staged, pidFile, content);
|
|
21051
21359
|
return true;
|
|
21360
|
+
} catch (error) {
|
|
21361
|
+
if (errorCode(error) === "EEXIST") {
|
|
21362
|
+
return false;
|
|
21363
|
+
}
|
|
21364
|
+
throw error;
|
|
21365
|
+
} finally {
|
|
21366
|
+
await unlinkQuietly(staged);
|
|
21367
|
+
}
|
|
21368
|
+
}
|
|
21369
|
+
async function linkExclusive(source, target, content) {
|
|
21370
|
+
try {
|
|
21371
|
+
await link(source, target);
|
|
21372
|
+
} catch (error) {
|
|
21373
|
+
if (!HARD_LINK_UNSUPPORTED.has(errorCode(error) ?? "")) {
|
|
21374
|
+
throw error;
|
|
21375
|
+
}
|
|
21376
|
+
await writeFile3(target, content, { encoding: "utf-8", flag: "wx" });
|
|
21377
|
+
}
|
|
21378
|
+
}
|
|
21379
|
+
async function replacePidFile(pidFile, content) {
|
|
21380
|
+
const staged = siblingPath(pidFile, "tmp");
|
|
21381
|
+
try {
|
|
21382
|
+
await writeFile3(staged, content, { encoding: "utf-8", flag: "wx" });
|
|
21383
|
+
await rename2(staged, pidFile);
|
|
21384
|
+
} catch (error) {
|
|
21385
|
+
await unlinkQuietly(staged);
|
|
21386
|
+
throw error;
|
|
21387
|
+
}
|
|
21388
|
+
}
|
|
21389
|
+
async function removePidFileIf(pidFile, expected) {
|
|
21390
|
+
await removePidFileWhen(pidFile, async (taken) => {
|
|
21391
|
+
try {
|
|
21392
|
+
return await readFile3(taken, "utf-8") === expected;
|
|
21393
|
+
} catch {
|
|
21394
|
+
return false;
|
|
21395
|
+
}
|
|
21396
|
+
});
|
|
21397
|
+
}
|
|
21398
|
+
async function removeDanglingLink(pidFile) {
|
|
21399
|
+
if (!await isDanglingLink(pidFile)) {
|
|
21400
|
+
return;
|
|
21401
|
+
}
|
|
21402
|
+
await removePidFileWhen(pidFile, isDanglingLink);
|
|
21403
|
+
}
|
|
21404
|
+
async function isDanglingLink(path) {
|
|
21405
|
+
try {
|
|
21406
|
+
await readlink(path);
|
|
21052
21407
|
} catch {
|
|
21053
|
-
await cleanupPidFile(pidFile);
|
|
21054
21408
|
return false;
|
|
21055
21409
|
}
|
|
21410
|
+
try {
|
|
21411
|
+
await stat3(path);
|
|
21412
|
+
return false;
|
|
21413
|
+
} catch {
|
|
21414
|
+
return true;
|
|
21415
|
+
}
|
|
21056
21416
|
}
|
|
21057
|
-
async function
|
|
21417
|
+
async function removePidFileWhen(pidFile, isExpected) {
|
|
21418
|
+
const taken = siblingPath(pidFile, "taken");
|
|
21058
21419
|
try {
|
|
21059
|
-
await
|
|
21420
|
+
await rename2(pidFile, taken);
|
|
21421
|
+
} catch {
|
|
21422
|
+
return;
|
|
21423
|
+
}
|
|
21424
|
+
if (!await isExpected(taken)) {
|
|
21425
|
+
await putBack(taken, pidFile);
|
|
21426
|
+
}
|
|
21427
|
+
await unlinkQuietly(taken);
|
|
21428
|
+
}
|
|
21429
|
+
async function putBack(taken, pidFile) {
|
|
21430
|
+
try {
|
|
21431
|
+
await link(taken, pidFile);
|
|
21432
|
+
return;
|
|
21433
|
+
} catch (error) {
|
|
21434
|
+
if (!HARD_LINK_UNSUPPORTED.has(errorCode(error) ?? "")) {
|
|
21435
|
+
return;
|
|
21436
|
+
}
|
|
21437
|
+
}
|
|
21438
|
+
try {
|
|
21439
|
+
await writeFile3(pidFile, await readFile3(taken, "utf-8"), { encoding: "utf-8", flag: "wx" });
|
|
21440
|
+
} catch {}
|
|
21441
|
+
}
|
|
21442
|
+
async function unlinkQuietly(path) {
|
|
21443
|
+
try {
|
|
21444
|
+
await unlink(path);
|
|
21060
21445
|
} catch {}
|
|
21061
21446
|
}
|
|
21062
21447
|
|
|
@@ -21075,8 +21460,9 @@ async function serveCommand(options) {
|
|
|
21075
21460
|
runForeground(cwd, port, buildDirOpts);
|
|
21076
21461
|
return;
|
|
21077
21462
|
}
|
|
21463
|
+
let result;
|
|
21078
21464
|
try {
|
|
21079
|
-
await startServeBackground(cwd, port, buildDirOpts);
|
|
21465
|
+
result = await startServeBackground(cwd, port, buildDirOpts);
|
|
21080
21466
|
} catch (error) {
|
|
21081
21467
|
if (!(error instanceof ServePidFileError)) {
|
|
21082
21468
|
throw error;
|
|
@@ -21084,8 +21470,22 @@ async function serveCommand(options) {
|
|
|
21084
21470
|
console.error(describePidFileError(error));
|
|
21085
21471
|
process.exit(1);
|
|
21086
21472
|
}
|
|
21087
|
-
|
|
21088
|
-
|
|
21473
|
+
switch (result.status) {
|
|
21474
|
+
case "already-running":
|
|
21475
|
+
console.log(result.notPermitted === true ? i18n.t("cli.web.start.alreadyRunningNotPermitted", {
|
|
21476
|
+
pid: result.pid,
|
|
21477
|
+
path: String(resolveServePidFile())
|
|
21478
|
+
}) : i18n.t("cli.web.start.alreadyRunning", { pid: result.pid }));
|
|
21479
|
+
return;
|
|
21480
|
+
case "starting-elsewhere":
|
|
21481
|
+
console.log(i18n.t("cli.web.start.startingElsewhere"));
|
|
21482
|
+
return;
|
|
21483
|
+
case "build-missing":
|
|
21484
|
+
case "spawn-failed":
|
|
21485
|
+
console.error(i18n.t("cli.web.start.failed"));
|
|
21486
|
+
process.exit(1);
|
|
21487
|
+
}
|
|
21488
|
+
if (await getServerPid() === result.pid) {
|
|
21089
21489
|
console.log(i18n.t("cli.web.start.started", { port }));
|
|
21090
21490
|
} else {
|
|
21091
21491
|
console.error(i18n.t("cli.web.start.failed"));
|
|
@@ -21100,7 +21500,16 @@ function describePidFileError(error) {
|
|
|
21100
21500
|
return i18n.t("cli.web.start.pidNotWritable", { path: String(error.pidFile), error: cause });
|
|
21101
21501
|
}
|
|
21102
21502
|
async function serveStopCommand() {
|
|
21103
|
-
|
|
21503
|
+
let stopped;
|
|
21504
|
+
try {
|
|
21505
|
+
stopped = await stopServe();
|
|
21506
|
+
} catch (error) {
|
|
21507
|
+
if (!(error instanceof ServeStopPermissionError)) {
|
|
21508
|
+
throw error;
|
|
21509
|
+
}
|
|
21510
|
+
console.error(i18n.t("cli.web.stop.notPermitted", { pid: error.pid, path: error.pidFile }));
|
|
21511
|
+
process.exit(1);
|
|
21512
|
+
}
|
|
21104
21513
|
if (stopped) {
|
|
21105
21514
|
console.log(i18n.t("cli.web.stop.stopped"));
|
|
21106
21515
|
} else {
|