@triflux/remote 10.50.1 → 10.50.2

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.
@@ -143,7 +143,14 @@ function envWarning(server, home, env, warnings) {
143
143
  }
144
144
  }
145
145
 
146
- function patchJson(original, file, home, env, warnings) {
146
+ function patchJson(
147
+ original,
148
+ file,
149
+ home,
150
+ env,
151
+ warnings,
152
+ { withEnv = true } = {},
153
+ ) {
147
154
  const data = JSON.parse(original);
148
155
  if (!data || typeof data !== "object" || Array.isArray(data))
149
156
  throw new Error(`${file}: JSON 객체가 아닙니다`);
@@ -190,7 +197,7 @@ function patchJson(original, file, home, env, warnings) {
190
197
  ...kept,
191
198
  command: server.command,
192
199
  args: server.args,
193
- ...(server.envVars.length
200
+ ...(withEnv && server.envVars.length
194
201
  ? {
195
202
  env: {
196
203
  ...kept.env,
@@ -710,6 +717,8 @@ export function cleanupLegacyMcp({
710
717
  [join(repoRoot, ".mcp.json"), "json"],
711
718
  [join(home, ".codex/config.toml"), "toml"],
712
719
  [join(home, ".gemini/settings.json"), "json"],
720
+ // agy 는 \${VAR} 참조를 풀지 않으므로 env 없이 바꾸고 셸 환경을 물려받게 한다.
721
+ [join(home, ".gemini/config/mcp_config.json"), "agy"],
713
722
  ];
714
723
  const plans = [];
715
724
  let blocked = false;
@@ -721,7 +730,9 @@ export function cleanupLegacyMcp({
721
730
  const plan =
722
731
  kind === "toml"
723
732
  ? patchToml(original, file, home, env, result.warnings)
724
- : patchJson(original, file, home, env, result.warnings);
733
+ : patchJson(original, file, home, env, result.warnings, {
734
+ withEnv: kind !== "agy",
735
+ });
725
736
  if (plan.blocked) blocked = true;
726
737
  if (plan.count) plans.push({ file, target, original, ...plan });
727
738
  } catch {
@@ -10,7 +10,7 @@ export const PSMUX_REQUIRED_COMMANDS = [
10
10
 
11
11
  export const PSMUX_OPTIONAL_COMMANDS = ["detach-client"];
12
12
 
13
- // Windows 패키지 매니저 — winget/scoop/choco 는 Windows 전용
13
+ // Windows 전용 설치 명령
14
14
  export const PSMUX_INSTALL_COMMANDS = [
15
15
  "winget install psmux",
16
16
  "scoop install psmux",
@@ -25,50 +25,18 @@ export const PSMUX_UPDATE_COMMANDS = [
25
25
  "cargo install psmux --force",
26
26
  ];
27
27
 
28
- // macOS 전용 — brew 가 표준, cargo 는 cross-platform fallback
29
- export const PSMUX_INSTALL_COMMANDS_DARWIN = [
30
- "brew install psmux",
31
- "cargo install psmux",
32
- ];
33
-
34
- export const PSMUX_UPDATE_COMMANDS_DARWIN = [
35
- "brew upgrade psmux",
36
- "cargo install psmux --force",
37
- ];
38
-
39
- // Linux 전용 — cargo 만 cross-platform 지원, distro 패키지는 미제공
40
- export const PSMUX_INSTALL_COMMANDS_LINUX = ["cargo install psmux"];
41
-
42
- export const PSMUX_UPDATE_COMMANDS_LINUX = ["cargo install psmux --force"];
43
-
44
- // PR #258 OS-primary 분기 정책: macOS/Linux 는 tmux 가 POSIX 표준 primary,
45
- // psmux 는 선택형이다. 따라서 macOS/Linux 에서 "psmux 미설치"는 fallback 사유가
46
- // 아니며, tmux 까지 없을 때만 native/in-process fallback 을 고려한다.
47
- export const PSMUX_FALLBACK_NOTE_DARWIN =
48
- "macOS 에서는 tmux 가 표준 멀티플렉서이며, " +
49
- "psmux 미설치는 fallback 사유가 아닙니다. tmux 도 없을 때만 in-process/native fallback 을 사용합니다.";
50
-
51
- export const PSMUX_FALLBACK_NOTE_LINUX =
52
- "Linux 에서는 tmux 가 표준 멀티플렉서이며, " +
53
- "psmux 미설치는 fallback 사유가 아닙니다. tmux 도 없을 때만 in-process/native fallback 을 사용합니다.";
54
-
55
28
  export function getPsmuxInstallCommandsFor(platform = process.platform) {
56
- if (platform === "darwin") return PSMUX_INSTALL_COMMANDS_DARWIN;
57
- if (platform === "linux") return PSMUX_INSTALL_COMMANDS_LINUX;
58
- // win32 또는 기타 (BSD 등) → Windows 풀 리스트로 폴백 (변경 전 동작 유지)
59
- return PSMUX_INSTALL_COMMANDS;
29
+ if (platform === "win32") return PSMUX_INSTALL_COMMANDS;
30
+ return [platform === "darwin" ? "brew install tmux" : "apt install tmux"];
60
31
  }
61
32
 
62
33
  export function getPsmuxUpdateCommandsFor(platform = process.platform) {
63
- if (platform === "darwin") return PSMUX_UPDATE_COMMANDS_DARWIN;
64
- if (platform === "linux") return PSMUX_UPDATE_COMMANDS_LINUX;
65
- return PSMUX_UPDATE_COMMANDS;
66
- }
67
-
68
- export function getPsmuxFallbackNoteFor(platform = process.platform) {
69
- if (platform === "darwin") return PSMUX_FALLBACK_NOTE_DARWIN;
70
- if (platform === "linux") return PSMUX_FALLBACK_NOTE_LINUX;
71
- return null;
34
+ if (platform === "win32") return PSMUX_UPDATE_COMMANDS;
35
+ return [
36
+ platform === "darwin"
37
+ ? "brew upgrade tmux"
38
+ : "apt install --only-upgrade tmux",
39
+ ];
72
40
  }
73
41
 
74
42
  export function formatPsmuxCommandList(
@@ -82,10 +50,7 @@ export function formatPsmuxInstallGuidance(
82
50
  indent = "",
83
51
  platform = process.platform,
84
52
  ) {
85
- const commands = getPsmuxInstallCommandsFor(platform);
86
- const list = formatPsmuxCommandList(commands, indent);
87
- const note = getPsmuxFallbackNoteFor(platform);
88
- return note ? `${list}\n${indent}(${note})` : list;
53
+ return formatPsmuxCommandList(getPsmuxInstallCommandsFor(platform), indent);
89
54
  }
90
55
 
91
56
  export function formatPsmuxUpdateGuidance(
@@ -160,8 +125,8 @@ export function probePsmuxSupport(options = {}) {
160
125
  missingCommands,
161
126
  missingOptionalCommands,
162
127
  hasHelp: helpOutput.length > 0,
163
- installHint: formatPsmuxInstallGuidance(" "),
164
- updateHint: formatPsmuxUpdateGuidance(" "),
128
+ installHint: formatPsmuxInstallGuidance(" ", "win32"),
129
+ updateHint: formatPsmuxUpdateGuidance(" ", "win32"),
165
130
  };
166
131
  } catch {
167
132
  return {
@@ -173,8 +138,8 @@ export function probePsmuxSupport(options = {}) {
173
138
  missingCommands: [...PSMUX_REQUIRED_COMMANDS],
174
139
  missingOptionalCommands: [...PSMUX_OPTIONAL_COMMANDS],
175
140
  hasHelp: false,
176
- installHint: formatPsmuxInstallGuidance(" "),
177
- updateHint: formatPsmuxUpdateGuidance(" "),
141
+ installHint: formatPsmuxInstallGuidance(" ", "win32"),
142
+ updateHint: formatPsmuxUpdateGuidance(" ", "win32"),
178
143
  };
179
144
  }
180
145
  }
@@ -207,6 +172,8 @@ export function probePrimaryMultiplexerSupport(options = {}) {
207
172
  recommended: true,
208
173
  missingCommands: [],
209
174
  missingOptionalCommands: [],
175
+ installHint: formatPsmuxInstallGuidance(" ", platform),
176
+ updateHint: formatPsmuxUpdateGuidance(" ", platform),
210
177
  };
211
178
  } catch (error) {
212
179
  return {
@@ -218,6 +185,8 @@ export function probePrimaryMultiplexerSupport(options = {}) {
218
185
  recommended: false,
219
186
  missingCommands: ["tmux"],
220
187
  missingOptionalCommands: [],
188
+ installHint: formatPsmuxInstallGuidance(" ", platform),
189
+ updateHint: formatPsmuxUpdateGuidance(" ", platform),
221
190
  error: error?.message || String(error),
222
191
  };
223
192
  }
@@ -340,32 +340,6 @@ export function loadTemplatePartials(partialsDir) {
340
340
  return partials;
341
341
  }
342
342
 
343
- export function loadSkillManifest(skillDir) {
344
- const manifestPath = join(skillDir, "skill.json");
345
- if (!existsSync(manifestPath)) return null;
346
-
347
- const raw = readFileSync(manifestPath, "utf8");
348
- return JSON.parse(raw);
349
- }
350
-
351
- export function parseFrontmatterWithManifest(source, skillDir) {
352
- const manifest = skillDir ? loadSkillManifest(skillDir) : null;
353
- const { data: yamlData, body } = parseFrontmatter(source);
354
-
355
- if (!manifest) return { data: yamlData, body };
356
-
357
- const merged = { ...yamlData };
358
- if (manifest.name) merged.name = manifest.name;
359
- if (manifest.description) merged.description = manifest.description;
360
- if (manifest.triggers) merged.triggers = manifest.triggers;
361
- if (manifest.argument_hint) merged["argument-hint"] = manifest.argument_hint;
362
- if (manifest.internal != null) merged.internal = manifest.internal;
363
- if (manifest.deprecated != null) merged.deprecated = manifest.deprecated;
364
- if (manifest.superseded_by) merged["superseded-by"] = manifest.superseded_by;
365
-
366
- return { data: merged, body };
367
- }
368
-
369
343
  export function renderSkillTemplate(template, context = {}, options = {}) {
370
344
  const { partials = {}, includes = {}, includeBaseDir = "" } = options;
371
345
  return renderWithContext(template, context, partials, {
@@ -1,355 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import {
3
- copyFileSync,
4
- cpSync,
5
- existsSync,
6
- lstatSync,
7
- mkdirSync,
8
- renameSync,
9
- rmSync,
10
- statSync,
11
- unlinkSync,
12
- } from "node:fs";
13
- import {
14
- basename,
15
- dirname,
16
- isAbsolute,
17
- join,
18
- relative,
19
- resolve,
20
- } from "node:path";
21
-
22
- const ARCHIVABLE_ACTIONS = new Set([
23
- "archive_or_resume_session",
24
- "prune_superseded_checkpoint",
25
- ]);
26
-
27
- function shortHash(value) {
28
- const str = String(value ?? "");
29
- let h = 5381;
30
- for (let i = 0; i < str.length; i += 1) {
31
- h = (h * 33) ^ str.charCodeAt(i);
32
- }
33
- return (h >>> 0).toString(36);
34
- }
35
-
36
- function stewardRootHash(rootDir) {
37
- return createHash("sha256")
38
- .update(String(rootDir || ""))
39
- .digest("hex")
40
- .slice(0, 12);
41
- }
42
-
43
- function hygieneKeyForRow(row) {
44
- return `${row.kind}:${row.id}`;
45
- }
46
-
47
- function yyyymmdd(value) {
48
- const date = value ? new Date(value) : new Date();
49
- if (Number.isNaN(date.getTime())) return yyyymmdd(new Date().toISOString());
50
- return date.toISOString().slice(0, 10).replace(/-/gu, "");
51
- }
52
-
53
- function safeLabel(value) {
54
- return (
55
- String(value || "item")
56
- .toLowerCase()
57
- .replace(/[^a-z0-9._-]+/gu, "-")
58
- .replace(/^-+|-+$/gu, "")
59
- .slice(0, 80) || "item"
60
- );
61
- }
62
-
63
- function isInside(parent, child) {
64
- const rel = relative(resolve(parent), resolve(child));
65
- return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
66
- }
67
-
68
- function normalizeActiveSessions(active) {
69
- const sessions = Array.isArray(active)
70
- ? active
71
- : Array.isArray(active?.sessions)
72
- ? active.sessions
73
- : Array.isArray(active?.live_sessions)
74
- ? active.live_sessions
75
- : [];
76
- return sessions
77
- .map((session) => ({
78
- sessionId: String(session?.sessionId || session?.session_id || "").trim(),
79
- phase: String(
80
- session?.phase || session?.status || "active",
81
- ).toLowerCase(),
82
- }))
83
- .filter((session) => session.sessionId);
84
- }
85
-
86
- function isLiveOwned(row, activeSessions) {
87
- const rowSession = String(row?.session_id || row?.id || "").trim();
88
- if (!rowSession) return false;
89
- return activeSessions.some(
90
- (session) => session.sessionId === rowSession && session.phase !== "stale",
91
- );
92
- }
93
-
94
- function assertProjectHash(row, rootDir) {
95
- if (!row?.project_root_hash) return;
96
- const allowed = new Set([shortHash(rootDir), stewardRootHash(rootDir)]);
97
- if (!allowed.has(String(row.project_root_hash))) {
98
- throw new Error(
99
- `project_root_hash mismatch for ${hygieneKeyForRow(row)}: ${row.project_root_hash}`,
100
- );
101
- }
102
- }
103
-
104
- function assertLakePath(lakeRoot, path, label) {
105
- if (!isInside(lakeRoot, path)) {
106
- throw new Error(`${label} must be under lakeRoot: ${path}`);
107
- }
108
- }
109
-
110
- function sourceBytes(path) {
111
- const info = statSync(path);
112
- if (info.isFile()) return info.size;
113
- if (!info.isDirectory()) return info.size || 0;
114
- return 0;
115
- }
116
-
117
- function targetPathFor({ lakeRoot, row, sourcePath, now }) {
118
- const archiveDir = join(lakeRoot, "archive", yyyymmdd(now));
119
- const label = safeLabel(hygieneKeyForRow(row));
120
- return join(archiveDir, `${label}-${basename(sourcePath)}`);
121
- }
122
-
123
- function hasSecondActorApproval(opts = {}) {
124
- if (opts.humanAck === true || opts.humanGate === true) return true;
125
- const actorSessionId = String(opts.actorSessionId || "").trim();
126
- if (!actorSessionId) return false;
127
- const hygieneKey = opts.hygieneKey;
128
- const approvals = Array.isArray(opts.approvals) ? opts.approvals : [];
129
- return approvals.some((entry) => {
130
- const ref = entry?.ref && typeof entry.ref === "object" ? entry.ref : {};
131
- if (ref.hygiene_key !== hygieneKey) return false;
132
- const approverSession = String(ref?.actor?.session_id || "").trim();
133
- return Boolean(approverSession && approverSession !== actorSessionId);
134
- });
135
- }
136
-
137
- function movePath(sourcePath, targetPath, fsOps = {}) {
138
- const ops = {
139
- copyFileSync,
140
- cpSync,
141
- existsSync,
142
- lstatSync,
143
- mkdirSync,
144
- renameSync,
145
- rmSync,
146
- unlinkSync,
147
- ...fsOps,
148
- };
149
- ops.mkdirSync(dirname(targetPath), { recursive: true });
150
- try {
151
- ops.renameSync(sourcePath, targetPath);
152
- return "rename";
153
- } catch (error) {
154
- if (error?.code !== "EXDEV") throw error;
155
- const info = ops.lstatSync(sourcePath);
156
- if (info.isDirectory()) {
157
- ops.cpSync(sourcePath, targetPath, { recursive: true, force: false });
158
- ops.rmSync(sourcePath, { recursive: true, force: false });
159
- } else {
160
- ops.copyFileSync(sourcePath, targetPath);
161
- ops.unlinkSync(sourcePath);
162
- }
163
- return "copy_unlink";
164
- }
165
- }
166
-
167
- function digestOperations(operations) {
168
- const payload = operations.map((operation) => ({
169
- hygiene_key: operation.hygiene_key,
170
- status: operation.status,
171
- source_path: operation.source_path,
172
- target_path: operation.target_path,
173
- bytes: operation.bytes,
174
- reason: operation.reason,
175
- }));
176
- return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
177
- }
178
-
179
- function rowsFromProjection(projection) {
180
- return Array.isArray(projection?.rows) ? projection.rows : [];
181
- }
182
-
183
- export async function applyHygieneArchiveActions({
184
- rootDir,
185
- lakeRoot,
186
- projection,
187
- dryRun = true,
188
- now = new Date().toISOString(),
189
- applyMode = "off",
190
- active = {},
191
- humanAck = false,
192
- humanGate = false,
193
- actorSessionId = null,
194
- approvals = [],
195
- fsOps = {},
196
- } = {}) {
197
- const archiveRoot = join(lakeRoot, "archive", yyyymmdd(now));
198
- assertLakePath(lakeRoot, archiveRoot, "archive target");
199
-
200
- const activeSessions = normalizeActiveSessions(active);
201
- const operations = [];
202
-
203
- for (const row of rowsFromProjection(projection)) {
204
- const hygieneKey = hygieneKeyForRow(row);
205
- if (row?.kind === "worktree" && row?.status === "orphaned") {
206
- operations.push({
207
- hygiene_key: hygieneKey,
208
- kind: row.kind,
209
- id: row.id,
210
- action: row.action,
211
- status: "blocked",
212
- reason: "orphan_worktree_requires_human",
213
- bytes: 0,
214
- });
215
- continue;
216
- }
217
-
218
- if (!ARCHIVABLE_ACTIONS.has(row?.action)) continue;
219
-
220
- assertProjectHash(row, rootDir);
221
-
222
- if (isLiveOwned(row, activeSessions)) {
223
- operations.push({
224
- hygiene_key: hygieneKey,
225
- kind: row.kind,
226
- id: row.id,
227
- action: row.action,
228
- status: "skipped",
229
- reason: "live_session_owned",
230
- bytes: 0,
231
- });
232
- continue;
233
- }
234
-
235
- const sourcePath = row.artifact_path ? resolve(row.artifact_path) : null;
236
- if (!sourcePath) {
237
- operations.push({
238
- hygiene_key: hygieneKey,
239
- kind: row.kind,
240
- id: row.id,
241
- action: row.action,
242
- status: "skipped",
243
- reason: "missing_artifact_path",
244
- bytes: 0,
245
- });
246
- continue;
247
- }
248
- // lake 밖 산출물은 자동 이동 대상이 아니다 — 크래시 대신 skip으로 보고한다.
249
- // (checkpoint 등 실제 산출물은 대부분 lake 밖에 있으므로 throw 하면 apply 전체가 죽는다.)
250
- if (!isInside(lakeRoot, sourcePath)) {
251
- operations.push({
252
- hygiene_key: hygieneKey,
253
- kind: row.kind,
254
- id: row.id,
255
- action: row.action,
256
- source_path: sourcePath,
257
- status: "skipped",
258
- reason: "source_outside_lake",
259
- bytes: 0,
260
- });
261
- continue;
262
- }
263
-
264
- const targetPath = targetPathFor({ lakeRoot, row, sourcePath, now });
265
- assertLakePath(lakeRoot, targetPath, "archive target");
266
-
267
- if (!existsSync(sourcePath)) {
268
- operations.push({
269
- hygiene_key: hygieneKey,
270
- kind: row.kind,
271
- id: row.id,
272
- action: row.action,
273
- source_path: sourcePath,
274
- target_path: targetPath,
275
- status: existsSync(targetPath) ? "already_archived" : "skipped",
276
- reason: existsSync(targetPath) ? undefined : "source_missing",
277
- bytes: 0,
278
- });
279
- continue;
280
- }
281
-
282
- operations.push({
283
- hygiene_key: hygieneKey,
284
- kind: row.kind,
285
- id: row.id,
286
- action: row.action,
287
- source_path: sourcePath,
288
- target_path: targetPath,
289
- status: "planned",
290
- bytes: sourceBytes(sourcePath),
291
- });
292
- }
293
-
294
- if (!dryRun) {
295
- for (const operation of operations) {
296
- if (operation.status !== "planned") continue;
297
- if (applyMode !== "archive") {
298
- operation.status = "blocked";
299
- operation.reason = "apply_mode_off";
300
- continue;
301
- }
302
- if (
303
- !hasSecondActorApproval({
304
- humanAck,
305
- humanGate,
306
- actorSessionId,
307
- approvals,
308
- hygieneKey: operation.hygiene_key,
309
- })
310
- ) {
311
- operation.status = "blocked";
312
- operation.reason = "second_actor_ack_required";
313
- continue;
314
- }
315
- operation.move_method = movePath(
316
- operation.source_path,
317
- operation.target_path,
318
- fsOps,
319
- );
320
- operation.status = "archived";
321
- }
322
- }
323
-
324
- const plannedCount = operations.filter((operation) =>
325
- ["planned", "archived", "already_archived"].includes(operation.status),
326
- ).length;
327
- const appliedCount = operations.filter(
328
- (operation) => operation.status === "archived",
329
- ).length;
330
- const blockedCount = operations.filter(
331
- (operation) => operation.status === "blocked",
332
- ).length;
333
-
334
- return {
335
- schema_version: "cto-hygiene-actions.v1",
336
- dry_run: Boolean(dryRun),
337
- apply_mode: applyMode,
338
- archive_root: archiveRoot,
339
- planned_count: plannedCount,
340
- applied_count: appliedCount,
341
- blocked_count: blockedCount,
342
- total_bytes: operations.reduce(
343
- (sum, operation) => sum + Number(operation.bytes || 0),
344
- 0,
345
- ),
346
- digest: digestOperations(operations),
347
- live_safety: {
348
- active_session_count: activeSessions.length,
349
- skipped_live_count: operations.filter(
350
- (operation) => operation.reason === "live_session_owned",
351
- ).length,
352
- },
353
- operations,
354
- };
355
- }