@yunhozz/arc 0.0.0-stage → 0.1.1

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.
Files changed (125) hide show
  1. package/MANIFEST.sha256 +96 -0
  2. package/README.md +61 -2
  3. package/bin/arc.mjs +3 -0
  4. package/claude/agents/arc-coder.md +67 -0
  5. package/claude/agents/arc-planner.md +74 -0
  6. package/claude/agents/arc-qa.md +68 -0
  7. package/claude/agents/arc-reviewer.md +65 -0
  8. package/claude/agents/arc-scouter.md +63 -0
  9. package/claude/skill/SKILL.md +370 -0
  10. package/claude/skill/hooks/git-policy.json +208 -0
  11. package/claude/skill/hooks/role-git-guard.mjs +98 -0
  12. package/claude/skill/models.toml +64 -0
  13. package/claude/skill/references/commit.md +77 -0
  14. package/claude/skill/references/git-recovery.md +46 -0
  15. package/claude/skill/references/handoff-prep.md +102 -0
  16. package/claude/skill/references/handoff.md +100 -0
  17. package/claude/skill/references/limits.md +42 -0
  18. package/claude/skill/references/planning.md +250 -0
  19. package/claude/skill/references/superpowers/LICENSE +21 -0
  20. package/claude/skill/references/superpowers/brainstorming/SKILL.md +225 -0
  21. package/claude/skill/references/superpowers/brainstorming/builder-check-prompt.md +23 -0
  22. package/claude/skill/references/superpowers/brainstorming/scripts/frame-template.html +213 -0
  23. package/claude/skill/references/superpowers/brainstorming/scripts/helper.js +167 -0
  24. package/claude/skill/references/superpowers/brainstorming/scripts/server.cjs +723 -0
  25. package/claude/skill/references/superpowers/brainstorming/scripts/start-server.sh +209 -0
  26. package/claude/skill/references/superpowers/brainstorming/scripts/stop-server.sh +120 -0
  27. package/claude/skill/references/superpowers/brainstorming/visual-companion.md +299 -0
  28. package/claude/skill/references/superpowers/provenance.json +26 -0
  29. package/claude/skill/references/superpowers/test-driven-development/SKILL.md +330 -0
  30. package/claude/skill/references/superpowers/test-driven-development/writing-good-tests.md +198 -0
  31. package/claude/skill/references/superpowers/writing-plans/SKILL.md +204 -0
  32. package/claude/skill/references/verification.md +125 -0
  33. package/claude/skill/references/visual-companion.md +16 -0
  34. package/claude/skill/scripts/apply-models.py +31 -0
  35. package/claude/skill/scripts/model_config.py +211 -0
  36. package/codex/agents/arc-coder.toml +37 -0
  37. package/codex/agents/arc-planner.toml +38 -0
  38. package/codex/agents/arc-qa.toml +24 -0
  39. package/codex/agents/arc-reviewer.toml +25 -0
  40. package/codex/agents/arc-scouter.toml +37 -0
  41. package/codex/skill/SKILL.md +356 -0
  42. package/codex/skill/models.toml +64 -0
  43. package/codex/skill/references/commit.md +100 -0
  44. package/codex/skill/references/git-recovery.md +64 -0
  45. package/codex/skill/references/handoff-prep.md +153 -0
  46. package/codex/skill/references/handoff-worktree.md +93 -0
  47. package/codex/skill/references/handoff.md +181 -0
  48. package/codex/skill/references/limits.md +43 -0
  49. package/codex/skill/references/planning.md +287 -0
  50. package/codex/skill/references/superpowers/LICENSE +21 -0
  51. package/codex/skill/references/superpowers/brainstorming/SKILL.md +225 -0
  52. package/codex/skill/references/superpowers/brainstorming/builder-check-prompt.md +23 -0
  53. package/codex/skill/references/superpowers/brainstorming/scripts/frame-template.html +213 -0
  54. package/codex/skill/references/superpowers/brainstorming/scripts/helper.js +167 -0
  55. package/codex/skill/references/superpowers/brainstorming/scripts/server.cjs +723 -0
  56. package/codex/skill/references/superpowers/brainstorming/scripts/start-server.sh +209 -0
  57. package/codex/skill/references/superpowers/brainstorming/scripts/stop-server.sh +120 -0
  58. package/codex/skill/references/superpowers/brainstorming/visual-companion.md +299 -0
  59. package/codex/skill/references/superpowers/provenance.json +26 -0
  60. package/codex/skill/references/superpowers/test-driven-development/SKILL.md +330 -0
  61. package/codex/skill/references/superpowers/test-driven-development/writing-good-tests.md +198 -0
  62. package/codex/skill/references/superpowers/writing-plans/SKILL.md +204 -0
  63. package/codex/skill/references/verification.md +163 -0
  64. package/codex/skill/references/visual-companion.md +23 -0
  65. package/codex/skill/scripts/apply-models.py +207 -0
  66. package/codex/skill/scripts/model_config.py +362 -0
  67. package/install.command +5 -0
  68. package/lib/cli/choices.mjs +47 -0
  69. package/lib/cli/completion.mjs +53 -0
  70. package/lib/cli/index.mjs +55 -0
  71. package/lib/cli/preset.mjs +29 -0
  72. package/lib/cli/progress.mjs +10 -0
  73. package/lib/cli/root.mjs +20 -0
  74. package/lib/cli/uninstall.mjs +17 -0
  75. package/lib/installer/acquisition.mjs +77 -0
  76. package/lib/installer/global-config.mjs +43 -0
  77. package/lib/installer/install.mjs +28 -0
  78. package/lib/installer/language.mjs +13 -0
  79. package/lib/installer/limits.mjs +21 -0
  80. package/lib/installer/manifest.mjs +37 -0
  81. package/lib/installer/package-references.mjs +32 -0
  82. package/lib/installer/plan.mjs +62 -0
  83. package/lib/installer/preset.mjs +26 -0
  84. package/lib/installer/runtime-config.mjs +85 -0
  85. package/lib/installer/shell-completion.mjs +29 -0
  86. package/lib/installer/stage-cleanup.mjs +40 -0
  87. package/lib/installer/superpowers.mjs +113 -0
  88. package/lib/installer/targets.mjs +55 -0
  89. package/lib/installer/transaction.mjs +91 -0
  90. package/lib/installer/uninstall.mjs +32 -0
  91. package/lib/limits.mjs +13 -0
  92. package/lib/models/config.mjs +58 -0
  93. package/lib/models/index.mjs +2 -0
  94. package/lib/models/profiles.mjs +111 -0
  95. package/lib/policy/claude-hook.mjs +40 -0
  96. package/lib/policy/render.mjs +74 -0
  97. package/lib/policy/role-policy.json +577 -0
  98. package/lib/safe-fs/index.mjs +67 -0
  99. package/lib/safe-fs/native.mjs +54 -0
  100. package/native/binding.c +157 -0
  101. package/native/binding.gyp +1 -0
  102. package/package.json +43 -4
  103. package/prebuilds/darwin-arm64/arc_safe_fs.node +0 -0
  104. package/prebuilds/darwin-x64/arc_safe_fs.node +0 -0
  105. package/prebuilds/index.json +64 -0
  106. package/prebuilds/linux-arm64-gnu/arc_safe_fs.node +0 -0
  107. package/prebuilds/linux-x64-gnu/arc_safe_fs.node +0 -0
  108. package/scripts/build-native.mjs +36 -0
  109. package/scripts/check-installer-parity.mjs +59 -0
  110. package/scripts/update-manifest.mjs +28 -0
  111. package/scripts/update-role-policy.mjs +15 -0
  112. package/scripts/update-superpowers.mjs +4 -0
  113. package/scripts/vendor-toml.mjs +40 -0
  114. package/vendor/toml/LICENSE +24 -0
  115. package/vendor/toml/dist/date.js +156 -0
  116. package/vendor/toml/dist/error.js +67 -0
  117. package/vendor/toml/dist/extract.js +293 -0
  118. package/vendor/toml/dist/index.js +34 -0
  119. package/vendor/toml/dist/parse.js +164 -0
  120. package/vendor/toml/dist/primitive.js +151 -0
  121. package/vendor/toml/dist/stringify.js +219 -0
  122. package/vendor/toml/dist/struct.js +156 -0
  123. package/vendor/toml/dist/util.js +57 -0
  124. package/vendor/toml/package.json +6 -0
  125. package/vendor/toml/provenance.json +35 -0
@@ -0,0 +1,362 @@
1
+ """Load ARC model settings and render model fields into TOML profiles."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import tomllib
8
+ import unicodedata
9
+ from pathlib import Path
10
+ from typing import TypedDict
11
+
12
+
13
+ ROLE_FILES: dict[str, str] = {
14
+ "arc_scouter": "arc-scouter.toml",
15
+ "arc_planner": "arc-planner.toml",
16
+ "arc_coder": "arc-coder.toml",
17
+ "arc_qa": "arc-qa.toml",
18
+ "arc_reviewer": "arc-reviewer.toml",
19
+ }
20
+
21
+
22
+ class RoleSettings(TypedDict):
23
+ model: str
24
+ model_reasoning_effort: str
25
+
26
+
27
+ class ValidationError(Exception):
28
+ """An invalid model settings file or agent profile."""
29
+
30
+
31
+ _MODEL_FIELDS = ("model", "model_reasoning_effort")
32
+ _BARE_KEY = re.compile(r"[A-Za-z0-9_-]+")
33
+ _STRING_ASSIGNMENT = re.compile(
34
+ r"(?P<prefix>[ \t]*(?:model|model_reasoning_effort|\"model\"|\"model_reasoning_effort\"|"
35
+ r"'model'|'model_reasoning_effort')[ \t]*=[ \t]*)"
36
+ r'(?P<literal>"(?:\\.|[^"\\\r\n])*"|\x27[^\x27\r\n]*\x27)'
37
+ r"(?P<tail>[ \t]*(?:\#[^\r\n]*)?)(?P<newline>\r?\n)?\Z"
38
+ )
39
+
40
+
41
+ def _error(path: Path, key: str, message: str) -> ValidationError:
42
+ return ValidationError(f"{path}: {key}: {message}")
43
+
44
+
45
+ def _validated_string(value: object, path: Path, key: str) -> str:
46
+ if not isinstance(value, str):
47
+ raise _error(path, key, "must be a string")
48
+ if not value or value != value.strip():
49
+ raise _error(path, key, "must be nonempty and have no surrounding whitespace")
50
+ if any(unicodedata.category(char) == "Cc" or char in "\u2028\u2029" for char in value):
51
+ raise _error(path, key, "must not contain control characters or line breaks")
52
+ return value
53
+
54
+
55
+ def _decode_toml(content: bytes, path: Path, label: str) -> tuple[str, dict]:
56
+ try:
57
+ text = content.decode("utf-8")
58
+ except UnicodeDecodeError as exc:
59
+ raise _error(path, label, f"is not valid UTF-8: {exc}") from exc
60
+ try:
61
+ parsed = tomllib.loads(text)
62
+ except tomllib.TOMLDecodeError as exc:
63
+ raise _error(path, label, f"is not valid TOML: {exc}") from exc
64
+ return text, parsed
65
+
66
+
67
+ def _parse_file(path: Path, label: str) -> tuple[bytes, str, dict]:
68
+ try:
69
+ content = path.read_bytes()
70
+ except OSError as exc:
71
+ raise _error(path, label, f"cannot read file: {exc}") from exc
72
+ text, parsed = _decode_toml(content, path, label)
73
+ return content, text, parsed
74
+
75
+
76
+ PRESETS = ("economy", "balanced", "smart")
77
+
78
+
79
+ def validate_config(config: dict, path: Path) -> dict:
80
+ if not isinstance(config, dict) or set(config) != {"version", "preset", "roles"} or type(config["version"]) is not int or config["version"] != 1:
81
+ raise _error(path, "settings", "update to version 1 unified presets; reinstall ARC or edit models.toml to include preset and all role presets")
82
+ if not isinstance(config["preset"], str) or config["preset"] not in PRESETS:
83
+ raise _error(path, "preset", "must be economy, balanced or smart")
84
+ roles = config["roles"]
85
+ if not isinstance(roles, dict) or set(roles) != set(ROLE_FILES):
86
+ raise _error(path, "roles", "must define exactly the five ARC roles")
87
+ for role, variants in roles.items():
88
+ if not isinstance(variants, dict) or set(variants) != set(PRESETS):
89
+ raise _error(path, f"roles.{role}", "must contain exactly economy, balanced and smart")
90
+ for preset, values in variants.items():
91
+ _settings_for_profile(values, f"{role}.{preset}", path)
92
+ return config
93
+
94
+
95
+ def load_settings(config_path: Path) -> dict[str, RoleSettings]:
96
+ """Validate every preset and return the active role settings."""
97
+ _, _, config = _parse_file(config_path, "settings")
98
+ validate_config(config, config_path)
99
+ return {role: dict(variants[config["preset"]]) for role, variants in config["roles"].items()}
100
+
101
+
102
+ def serialize_settings(config: dict) -> bytes:
103
+ """Serialize a complete unified configuration without losing inactive presets."""
104
+ validate_config(config, Path("models.toml"))
105
+ lines = ["version = 1", f'preset = {json.dumps(config["preset"])}']
106
+ for role in ROLE_FILES:
107
+ for preset in PRESETS:
108
+ lines.extend(["", f"[roles.{role}.{preset}]"])
109
+ lines.extend(f"{key} = {json.dumps(config['roles'][role][preset][key], ensure_ascii=False)}" for key in _MODEL_FIELDS)
110
+ return ("\n".join(lines) + "\n").encode("utf-8")
111
+
112
+
113
+ def _physical_lines(text: str) -> list[tuple[str, int]]:
114
+ """Return physical LF/CRLF lines with their starting character offsets."""
115
+ lines: list[tuple[str, int]] = []
116
+ start = 0
117
+ while start < len(text):
118
+ newline = text.find("\n", start)
119
+ end = len(text) if newline < 0 else newline + 1
120
+ lines.append((text[start:end], start))
121
+ start = end
122
+ return lines
123
+
124
+
125
+ def _quoted_key_value(token: str) -> str | None:
126
+ try:
127
+ parsed = tomllib.loads(f"{token} = 0")
128
+ except tomllib.TOMLDecodeError:
129
+ return None
130
+ if len(parsed) != 1:
131
+ return None
132
+ return next(iter(parsed))
133
+
134
+
135
+ def _assignment_key(line: str) -> str | None:
136
+ """Read one root assignment key token from a line, if it is a plain assignment."""
137
+ cursor = len(line) - len(line.lstrip(" \t"))
138
+ if cursor >= len(line) or line[cursor] == "#":
139
+ return None
140
+ start = cursor
141
+ first = line[cursor]
142
+ if first in "\"'":
143
+ quote = first
144
+ cursor += 1
145
+ while cursor < len(line):
146
+ char = line[cursor]
147
+ if quote == '"' and char == "\\":
148
+ cursor += 2
149
+ continue
150
+ if char == quote:
151
+ cursor += 1
152
+ break
153
+ if char in "\r\n":
154
+ return None
155
+ cursor += 1
156
+ else:
157
+ return None
158
+ else:
159
+ match = _BARE_KEY.match(line, cursor)
160
+ if match is None:
161
+ return None
162
+ cursor = match.end()
163
+ token = line[start:cursor]
164
+ while cursor < len(line) and line[cursor] in " \t":
165
+ cursor += 1
166
+ if cursor >= len(line) or line[cursor] != "=":
167
+ return None
168
+ return token if token in _MODEL_FIELDS else _quoted_key_value(token)
169
+
170
+
171
+ def _multiline_close(line: str, quote: str, basic: bool) -> int | None:
172
+ """Find a TOML multiline string terminator in a physical line."""
173
+ cursor = 0
174
+ while cursor < len(line):
175
+ start = line.find(quote, cursor)
176
+ if start < 0:
177
+ return None
178
+ end = start
179
+ while end < len(line) and line[end] == quote[0]:
180
+ end += 1
181
+ count = end - start
182
+ if count >= 3:
183
+ close_start = end - 3
184
+ if basic:
185
+ slash_count = 0
186
+ before = close_start - 1
187
+ while before >= 0 and line[before] == "\\":
188
+ slash_count += 1
189
+ before -= 1
190
+ if slash_count % 2:
191
+ cursor = end
192
+ continue
193
+ return close_start
194
+ cursor = end
195
+ return None
196
+
197
+
198
+ def _scan_root(text: str) -> tuple[dict[str, list[int]], int | None]:
199
+ """Locate model assignments before the first real table header."""
200
+ assignments: dict[str, list[int]] = {key: [] for key in _MODEL_FIELDS}
201
+ square_depth = 0
202
+ brace_depth = 0
203
+ multiline: str | None = None
204
+ multiline_basic = False
205
+ lines = _physical_lines(text)
206
+
207
+ for index, (physical, offset) in enumerate(lines):
208
+ body = physical[:-2] if physical.endswith("\r\n") else physical[:-1] if physical.endswith("\n") else physical
209
+ if multiline is None and square_depth == 0 and brace_depth == 0:
210
+ first = len(body) - len(body.lstrip(" \t"))
211
+ if first < len(body) and body[first] == "[":
212
+ return assignments, offset
213
+ key = _assignment_key(body)
214
+ if key in assignments:
215
+ assignments[key].append(index)
216
+
217
+ cursor = 0
218
+ while cursor < len(body):
219
+ if multiline is not None:
220
+ close_at = _multiline_close(body[cursor:], multiline, multiline_basic)
221
+ if close_at is None:
222
+ break
223
+ cursor += close_at + 3
224
+ multiline = None
225
+ multiline_basic = False
226
+ continue
227
+
228
+ char = body[cursor]
229
+ if char == "#":
230
+ break
231
+ if body.startswith('"""', cursor) or body.startswith("'''", cursor):
232
+ multiline = body[cursor : cursor + 3]
233
+ multiline_basic = multiline == '"""'
234
+ cursor += 3
235
+ continue
236
+ if char == '"' or char == "'":
237
+ quote = char
238
+ cursor += 1
239
+ while cursor < len(body):
240
+ if quote == '"' and body[cursor] == "\\":
241
+ cursor += 2
242
+ elif body[cursor] == quote:
243
+ cursor += 1
244
+ break
245
+ else:
246
+ cursor += 1
247
+ continue
248
+ if char == "[":
249
+ square_depth += 1
250
+ elif char == "]":
251
+ square_depth = max(0, square_depth - 1)
252
+ elif char == "{":
253
+ brace_depth += 1
254
+ elif char == "}":
255
+ brace_depth = max(0, brace_depth - 1)
256
+ cursor += 1
257
+
258
+ return assignments, None
259
+
260
+
261
+ def _profile_parts(profile_bytes: bytes, role: str, source_path: Path) -> tuple[str, dict, dict[str, list[int]], int | None, list[tuple[str, int]]]:
262
+ if role not in ROLE_FILES:
263
+ raise _error(source_path, "role", f"unknown ARC role {role!r}")
264
+ text, profile = _decode_toml(profile_bytes, source_path, "profile")
265
+ if profile.get("name") != role:
266
+ raise _error(source_path, "name", f"expected profile name {role!r}, found {profile.get('name')!r}")
267
+ assignments, first_table = _scan_root(text)
268
+ return text, profile, assignments, first_table, _physical_lines(text)
269
+
270
+
271
+ def validate_template(profile_bytes: bytes, role: str, source_path: Path) -> None:
272
+ """Validate a model-free source profile template without modifying it."""
273
+ _, profile, assignments, _, _ = _profile_parts(profile_bytes, role, source_path)
274
+ for key in _MODEL_FIELDS:
275
+ if key in profile or assignments[key]:
276
+ raise _error(source_path, key, "must be absent from the root of a source template")
277
+
278
+
279
+ def _settings_for_profile(settings: RoleSettings, role: str, source_path: Path) -> RoleSettings:
280
+ if not isinstance(settings, dict) or set(settings) != set(_MODEL_FIELDS):
281
+ raise _error(source_path, f"roles.{role}", "must contain only 'model' and 'model_reasoning_effort'")
282
+ return {
283
+ "model": _validated_string(settings["model"], source_path, f"roles.{role}.model"),
284
+ "model_reasoning_effort": _validated_string(
285
+ settings["model_reasoning_effort"], source_path, f"roles.{role}.model_reasoning_effort"
286
+ ),
287
+ }
288
+
289
+
290
+ def _newline(text: str) -> str:
291
+ match = re.search(r"\r\n|\n", text)
292
+ return match.group(0) if match else "\n"
293
+
294
+
295
+ def _replace_line_literal(line: str, key: str, value: str, source_path: Path) -> str:
296
+ match = _STRING_ASSIGNMENT.fullmatch(line)
297
+ if match is None:
298
+ raise _error(source_path, key, "must be a one-line quoted string assignment")
299
+ prefix = match.group("prefix")
300
+ # A quoted key may resolve to this name while using a noncanonical escape;
301
+ # that form is valid TOML but not safe to edit textually.
302
+ key_text = prefix.split("=", 1)[0].strip()
303
+ if key_text not in (key, f'"{key}"', f"'{key}'"):
304
+ raise _error(source_path, key, "uses an unsupported quoted key spelling")
305
+ try:
306
+ current = tomllib.loads(f"{key} = {match.group('literal')}\n")[key]
307
+ except tomllib.TOMLDecodeError as exc:
308
+ raise _error(source_path, key, f"cannot parse quoted string assignment: {exc}") from exc
309
+ if not isinstance(current, str):
310
+ raise _error(source_path, key, "must be a quoted string assignment")
311
+ if current == value:
312
+ return line
313
+ return prefix + json.dumps(value, ensure_ascii=False) + match.group("tail") + (match.group("newline") or "")
314
+
315
+
316
+ def render_profile(profile_bytes: bytes, role: str, settings: RoleSettings, source_path: Path) -> bytes:
317
+ """Return valid profile TOML bytes with the role's model fields rendered."""
318
+ text, profile, assignments, first_table, lines = _profile_parts(profile_bytes, role, source_path)
319
+ validated_settings = _settings_for_profile(settings, role, source_path)
320
+ line_values = [line for line, _ in lines]
321
+ missing: list[str] = []
322
+
323
+ for key in _MODEL_FIELDS:
324
+ present = key in profile
325
+ found = assignments[key]
326
+ if not present:
327
+ if found:
328
+ raise _error(source_path, key, "root assignment could not be matched to the parsed TOML value")
329
+ missing.append(key)
330
+ continue
331
+ if not isinstance(profile[key], str):
332
+ raise _error(source_path, key, "root value must be a one-line quoted string assignment")
333
+ if len(found) != 1:
334
+ raise _error(source_path, key, f"expected one safe root assignment, found {len(found)}")
335
+ index = found[0]
336
+ updated = _replace_line_literal(line_values[index], key, validated_settings[key], source_path)
337
+ line_values[index] = updated
338
+
339
+ rendered = "".join(line_values)
340
+ if missing:
341
+ newline = _newline(text)
342
+ addition = newline.join(f"{key} = {json.dumps(validated_settings[key], ensure_ascii=False)}" for key in missing)
343
+ addition += newline
344
+ if first_table is not None:
345
+ table_line = next(index for index, (_, offset) in enumerate(lines) if offset == first_table)
346
+ insertion = sum(len(line) for line in line_values[:table_line])
347
+ prefix = rendered[:insertion]
348
+ if prefix and not prefix.endswith(("\n", "\r")):
349
+ addition = newline + addition
350
+ rendered = rendered[:insertion] + addition + rendered[insertion:]
351
+ else:
352
+ if rendered and not rendered.endswith(("\n", "\r")):
353
+ addition = newline + addition
354
+ rendered += addition
355
+
356
+ try:
357
+ checked = tomllib.loads(rendered)
358
+ except tomllib.TOMLDecodeError as exc:
359
+ raise _error(source_path, "rendered profile", f"is not valid TOML: {exc}") from exc
360
+ if checked.get("name") != role or any(checked.get(key) != value for key, value in validated_settings.items()):
361
+ raise _error(source_path, "rendered profile", "does not match the validated role and model settings")
362
+ return rendered.encode("utf-8")
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
5
+ exec node "$SCRIPT_DIR/bin/arc.mjs" "$@"
@@ -0,0 +1,47 @@
1
+ import readline from 'node:readline';
2
+ import {EventEmitter} from 'node:events';
3
+ function inputError(message){return Object.assign(new Error(message),{code:'ARC_CLI_INPUT'});}
4
+ function cancelled(){return Object.assign(new Error('Installation cancelled.'),{code:'ARC_CANCELLED'});}
5
+ export async function selectChoices(options,{input=process.stdin,output=process.stdout,targetOnly=false,languageDefault}={}){
6
+ if(languageDefault===undefined&&options.target&&(targetOnly||options.preset)||languageDefault!==undefined&&options.language)return options;
7
+ if(!input.isTTY||!output.isTTY)throw inputError(targetOnly?'Non-interactive execution requires --target':'Non-interactive execution requires --target and --preset');
8
+ const menus=languageDefault!==undefined?[['language',['English','한국어'],['en','ko'],languageDefault==='ko'?1:0]]:[['target',['Claude','Codex','All'],['claude','codex','all'],0],['preset',['Economy','Balanced','Smart'],['economy','balanced','smart'],1]].filter(([key])=>!options[key]&&(!targetOnly||key==='target'));
9
+ const wasRaw=input.isRaw,wasFlowing=input.readableFlowing,listeners=new Map(['data','newListener'].map(event=>[event,input.listeners(event)]));
10
+ let menu=0,index=menus[0][3];
11
+ const draw=(redraw=false)=>{const [key,labels]=menus[menu];if(redraw)output.write(`\x1b[${labels.length}A\r`);else output.write(`Select ${key==='target'?'an installation target':key==='language'?'an output language':'a performance preset'} [Up/Down]:\n`);output.write(labels.map((label,i)=>`\x1b[2K${i===index?'>':' '} ${label}`).join('\n')+'\n');};
12
+ const keys=new EventEmitter();let onKey,onData,onEnd,onError,abort;
13
+ try{return await new Promise((resolve,reject)=>{
14
+ abort=()=>reject(cancelled());onEnd=abort;onError=reject;
15
+ onKey=(str,key={})=>{try{if(key.name==='c'&&key.ctrl||key.name==='d'&&key.ctrl)return abort();if(key.name==='up'||key.name==='down'){index=(index+(key.name==='up'?-1:1)+menus[menu][1].length)%menus[menu][1].length;draw(true);}else if(key.name==='return'||key.name==='enter'){const [name,,values]=menus[menu];options[name]=values[index];output.write('\n');if(++menu===menus.length){keys.removeListener('keypress',onKey);resolve(options);}else{index=menus[menu][3];draw();}}}catch(error){reject(error);}};
16
+ keys.on('keypress',onKey);readline.emitKeypressEvents(keys);onData=chunk=>keys.emit('data',chunk);input.on('data',onData);input.once('end',onEnd);input.once('error',onError);process.once('SIGINT',abort);
17
+ if(input.setRawMode)input.setRawMode(true);input.resume();draw();
18
+ });}finally{
19
+ keys.removeAllListeners();input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('error',onError);process.removeListener('SIGINT',abort);
20
+ for(const [event,before]of listeners)for(const listener of input.listeners(event))if(!before.includes(listener))input.removeListener(event,listener);
21
+ if(input.setRawMode)input.setRawMode(Boolean(wasRaw));if(wasFlowing!==true)input.pause();
22
+ }
23
+ }
24
+
25
+ export async function selectLimits(options,selections,{input=process.stdin,output=process.stdout}={}){
26
+ const{parsePositiveLimit}=await import('../limits.mjs');
27
+ const fields=[['maxTotalSubagents','max_total_subagents',100,'total'],['maxConcurrentSubagents','max_concurrent_subagents',5,'concurrent']].filter(([key])=>options[key]===undefined);
28
+ if(!fields.length)return options;
29
+ const wasRaw=input.isRaw,wasFlowing=input.readableFlowing,listeners=new Map(['data','newListener'].map(event=>[event,input.listeners(event)]));
30
+ let index=0,value='',onKey,onData,onEnd,onError,abort;
31
+ const draw=()=>{const[,,fallback,label]=fields[index];output.write(`Maximum ${label} subagents [Default=${fallback}]: `);};
32
+ try{return await new Promise((resolve,reject)=>{
33
+ abort=()=>reject(cancelled());onEnd=abort;onError=reject;
34
+ onKey=(str,key={})=>{if(key.ctrl&&(key.name==='c'||key.name==='d'))return abort();if(key.name==='return'||key.name==='enter'){
35
+ const[name,,,label]=fields[index];if(value.trim()){try{options[name]=parsePositiveLimit(value,label);}catch(cause){output.write(`\n${cause.message}\n`);value='';draw();return;}}
36
+ if(name==='maxConcurrentSubagents')options.concurrentSelected=true;output.write('\n');value='';if(++index===fields.length){input.removeListener('keypress',onKey);resolve(options);}else draw();
37
+ }else if(key.name==='backspace'){value=value.slice(0,-1);output.write('\b \b');}else if(str&&!key.ctrl&&!key.meta){value+=str;output.write(str);}};
38
+ onData=chunk=>{for(const character of chunk.toString()){if(index===fields.length)break;const name=character==='\r'||character==='\n'?'return':character==='\x03'?'c':character==='\x04'?'d':character==='\x7f'||character==='\b'?'backspace':undefined;onKey(character,{name,ctrl:character==='\x03'||character==='\x04'});}};
39
+ input.on('data',onData);input.once('end',onEnd);input.once('error',onError);process.once('SIGINT',abort);if(input.setRawMode)input.setRawMode(true);input.resume();draw();
40
+ });}finally{
41
+ input.removeListener('data',onData);input.removeListener('end',onEnd);input.removeListener('error',onError);process.removeListener('SIGINT',abort);
42
+ for(const[event,before]of listeners)for(const listener of input.listeners(event))if(!before.includes(listener))input.removeListener(event,listener);
43
+ if(input.setRawMode)input.setRawMode(Boolean(wasRaw));if(wasFlowing!==true)input.pause();
44
+ }
45
+ }
46
+
47
+ export async function selectLanguage(options,languageDefault,io={}){return selectChoices(options,{...io,languageDefault});}
@@ -0,0 +1,53 @@
1
+ export function renderCompletion(shell){
2
+ if(shell==='bash')return `_arc() {
3
+ local cur="\${COMP_WORDS[COMP_CWORD]}" prev="\${COMP_WORDS[COMP_CWORD-1]}" command="\${COMP_WORDS[1]}" choices=""
4
+ case "$prev" in
5
+ --target) choices="claude codex all" ;;
6
+ --preset) choices="economy balanced smart" ;;
7
+ --language) choices="en ko" ;;
8
+ *)
9
+ if (( COMP_CWORD == 1 )) && [[ $cur != -* ]]; then choices="preset uninstall"
10
+ else
11
+ case "$command" in
12
+ preset)
13
+ if (( COMP_CWORD == 2 )) && [[ $cur != -* ]]; then choices="economy balanced smart"
14
+ else choices="--target --check --dry-run --help"; fi ;;
15
+ uninstall) choices="--target --dry-run --skip-global --help --version" ;;
16
+ completion) (( COMP_CWORD == 2 )) && choices="zsh bash" ;;
17
+ -*) choices="--target --preset --language --max-total-subagents --max-concurrent-subagents --check --dry-run --skip-global --help --version" ;;
18
+ esac
19
+ fi ;;
20
+ esac
21
+ COMPREPLY=( $(compgen -W "$choices" -- "$cur") )
22
+ }
23
+ complete -F _arc arc
24
+ `;
25
+ if(shell==='zsh')return `_arc() {
26
+ local -a candidates
27
+ if (( CURRENT == 2 )) && [[ \${words[CURRENT]} != -* ]]; then
28
+ candidates=(preset uninstall)
29
+ else
30
+ case "\${words[CURRENT-1]}" in
31
+ --target) candidates=(claude codex all) ;;
32
+ --preset) candidates=(economy balanced smart) ;;
33
+ --language) candidates=(en ko) ;;
34
+ *) case "\${words[2]}" in
35
+ preset)
36
+ if (( CURRENT == 3 )) && [[ \${words[CURRENT]} != -* ]]; then candidates=(economy balanced smart)
37
+ else candidates=(--target --check --dry-run --help); fi ;;
38
+ uninstall) candidates=(--target --dry-run --skip-global --help --version) ;;
39
+ completion) (( CURRENT == 3 )) && candidates=(zsh bash) ;;
40
+ -*) candidates=(--target --preset --language --max-total-subagents --max-concurrent-subagents --check --dry-run --skip-global --help --version) ;;
41
+ esac ;;
42
+ esac
43
+ fi
44
+ compadd -a candidates
45
+ }
46
+ if (( ! $+functions[compdef] )); then
47
+ autoload -Uz compinit
48
+ compinit -D
49
+ fi
50
+ compdef _arc arc
51
+ `;
52
+ throw new Error('Unsupported completion shell');
53
+ }
@@ -0,0 +1,55 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import {fileURLToPath} from 'node:url';
5
+ const packageRoot=fileURLToPath(new URL('../../',import.meta.url));
6
+ import{parsePositiveLimit}from'../limits.mjs';
7
+ const usage='Usage: arc --target <claude|codex|all> --preset <economy|balanced|smart> --language <en|ko>\n [--max-total-subagents <count>] [--max-concurrent-subagents <count>]\n [--check | --dry-run] [--skip-global] [--help] [--version]\n --max-total-subagents: positive integer (default: 100)\n --max-concurrent-subagents: positive integer (default: 5)';
8
+ function inputError(message){return Object.assign(new Error(message),{code:'ARC_CLI_INPUT'});}
9
+ function cancelled(){return Object.assign(new Error('Installation cancelled.'),{code:'ARC_CANCELLED'});}
10
+ export function parseArgs(args){
11
+ const options={},seen=new Set();const names={'--max-total-subagents':'maxTotalSubagents','--max-concurrent-subagents':'maxConcurrentSubagents','--target':'target','--preset':'preset','--language':'language','--skip-global':'skipGlobal','--check':'check','--dry-run':'dryRun','--help':'help','--version':'version'};
12
+ for(let i=0;i<args.length;i++){const flag=args[i],key=Object.hasOwn(names,flag)?names[flag]:undefined;if(!key)throw inputError(`Unknown argument: ${flag}`);if(seen.has(flag))throw inputError(`Duplicate argument: ${flag}`);seen.add(flag);if(key==='maxTotalSubagents'||key==='maxConcurrentSubagents'){try{options[key]=parsePositiveLimit(args[++i],flag);}catch(cause){throw inputError(cause.message);}}else if(key==='target'||key==='preset'||key==='language'){const value=args[++i],allowed=key==='target'?['claude','codex','all']:key==='preset'?['economy','balanced','smart']:['en','ko'];if(!allowed.includes(value))throw inputError(`Invalid ${key}: ${value??'missing value'}`);options[key]=value;}else options[key]=true;}
13
+ if(options.check&&options.dryRun)throw inputError('--check and --dry-run cannot be combined');return options;
14
+ }
15
+ export async function selectChoices(options,io){return(await import('./choices.mjs')).selectChoices(options,io);}
16
+ export async function runCli(args=process.argv.slice(2),{input=process.stdin,output=process.stdout,error=process.stderr}={}){
17
+ let controller,onInterrupt,progress;
18
+ try{
19
+ const options=parseArgs(args);if(options.help){output.write(usage+'\n');return 0;}if(options.version){output.write(JSON.parse(fs.readFileSync(path.join(packageRoot,'package.json'))).version+'\n');return 0;}
20
+ if((!input.isTTY||!output.isTTY)&&(!options.target||!options.preset||!options.language))throw inputError('Non-interactive execution requires --target, --preset and --language');
21
+ await selectChoices(options,{input,output});
22
+ const {startProgress}=await import('./progress.mjs');
23
+ const interactive=input.isTTY&&output.isTTY,label=options.check?'Checking ARC…':options.dryRun?'Preparing ARC dry run…':'Installing ARC…';
24
+ if(!interactive)progress=startProgress({output,label});
25
+ controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);
26
+ const {loadNative}=await import('../safe-fs/native.mjs');const native=loadNative();
27
+ const [{openRoot},{verifyPayload},{resolveTarget},{buildInstallPlan,validatePlanSet}]=await Promise.all([import('../safe-fs/index.mjs'),import('../installer/manifest.mjs'),import('../installer/targets.mjs'),import('../installer/plan.mjs')]);
28
+ const safeFs={openRoot:p=>openRoot(p,{native})},payload=verifyPayload({packageRoot,safeFs});
29
+ const platforms=options.target==='all'?['codex','claude']:[options.target];
30
+ const {previewLimits,previewLanguage}=await import('../installer/plan.mjs');const{defaultLanguage}=await import('../installer/language.mjs');const selections=platforms.map(platform=>{const target=resolveTarget(platform,{userHome:os.homedir(),env:process.env});return{target,preview:previewLimits({target,safeFs}),languagePreview:previewLanguage({target,safeFs})};});
31
+ if(interactive){const{selectLimits,selectLanguage}=await import('./choices.mjs');await selectLimits(options,selections,{input,output});await selectLanguage(options,defaultLanguage(selections.map(item=>item.languagePreview.existing)),{input,output});}
32
+ progress??=startProgress({output,label});
33
+ const plans=selections.map(({target,preview,languagePreview})=>buildInstallPlan({payload,target,language:options.language,languagePreview,preset:options.preset,skipGlobal:options.skipGlobal,safeFs,limitsPreview:preview,limits:{...(options.maxTotalSubagents===undefined?{}:{max_total_subagents:options.maxTotalSubagents}),...(options.maxConcurrentSubagents===undefined?{}:{max_concurrent_subagents:options.maxConcurrentSubagents})},concurrentExplicit:options.maxConcurrentSubagents!==undefined||Boolean(options.concurrentSelected)}));const {attachShellCompletion}=await import('../installer/shell-completion.mjs');attachShellCompletion(plans,{safeFs});validatePlanSet(plans);
34
+ const languageLabel=options.language==='ko'?'한국어':'English';
35
+ progress.write(`Install: ${platforms.join('/')} · ${options.preset} · ${languageLabel}\n`);
36
+ for(const plan of plans)progress.write(`${plan.target.platform}: ${plan.target.home} · total ${plan.limits.max_total_subagents} · concurrent ${plan.limits.max_concurrent_subagents}\n`);
37
+ if(options.skipGlobal)progress.write('Global settings: skipped\n');
38
+ else progress.write('zsh completion: eval "$(arc completion zsh)" for this session\n');
39
+ if(plans.some(plan=>plan.limits.max_concurrent_subagents>plan.limits.max_total_subagents))progress.write(`Note: fresh ARC sessions cap concurrency at ${Math.min(...plans.map(plan=>plan.limits.max_total_subagents))}.\n`);
40
+ if(options.dryRun)for(const plan of plans){
41
+ for(const name of plan.generated.skill.keys())progress.write(`Would write file: ${path.join(plan.target.home,'skills/arc',name)}\n`);
42
+ for(const name of plan.generated.profiles.keys())progress.write(`Would write file: ${path.join(plan.target.home,'agents',name)}\n`);
43
+ for(const change of plan.globalChanges)progress.write(`Would update global file: ${change.path}\n`);
44
+ }
45
+ if(options.check||options.dryRun){progress.stop();output.write(options.check?'Check completed. No changes made.\n':'Dry run completed. No changes made.\n');return 0;}
46
+ const [{installTargets},{acquireExternalSkills}]=await Promise.all([import('../installer/install.mjs'),import('../installer/acquisition.mjs')]);
47
+ const result=await installTargets(plans,{safeFs,signal:controller.signal,acquisition:async()=>{const work=fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()),'arc-acquire-'));const acquired=await acquireExternalSkills({work,safeFs,signal:controller.signal});const dispose=acquired.dispose;return{...acquired,dispose:async()=>{await dispose();fs.rmdirSync(work);}};}});
48
+ progress.stop();
49
+ const reportResults=()=>{for(const item of result.results){output.write(`${item.platform}: ${item.status}; rollback: ${item.rollback}\n`);for(const recovery of item.residualPaths)output.write(`Residual path: ${recovery}\n`);if(item.error)error.write(`Error: ${item.error.message}\n`);}};
50
+ if(result.interrupted){reportResults();return 130;}
51
+ if(result.results.every(item=>item.status==='installed'&&!item.error)){output.write(`ARC installed! [${result.results.map(item=>item.platform).join('/')}]\n`);return 0;}
52
+ reportResults();return 1;
53
+ }catch(cause){progress?.stop();error.write(`Error: ${cause.message}\n`);if(cause.code==='ARC_CLI_INPUT'){error.write(usage+'\n');return 2;}if(cause.code==='ARC_CANCELLED')return 130;return ['ARC_UNSUPPORTED_RUNTIME','ARC_NATIVE_UNAVAILABLE','ARC_TARGET_CONFIGURATION'].includes(cause.code)?2:1;
54
+ }finally{progress?.stop();if(onInterrupt)process.removeListener('SIGINT',onInterrupt);}
55
+ }
@@ -0,0 +1,29 @@
1
+ import os from 'node:os';
2
+ import {validatePlanSet} from '../installer/plan.mjs';
3
+ import {selectChoices} from './choices.mjs';
4
+ const usage='Usage: arc preset [economy|balanced|smart] --target <codex|claude|all> [--check | --dry-run] [--help]';
5
+ const inputError=message=>Object.assign(new Error(message),{code:'ARC_CLI_INPUT'});
6
+ export function parseArgs(args){
7
+ const options={},seen=new Set();
8
+ for(let i=0;i<args.length;i++){
9
+ const arg=args[i];if(['economy','balanced','smart'].includes(arg)){if(options.preset)throw inputError('Duplicate preset');options.preset=arg;continue;}
10
+ if(!['--target','--check','--dry-run','--help'].includes(arg))throw inputError(`Unknown argument: ${arg}`);if(seen.has(arg))throw inputError(`Duplicate argument: ${arg}`);seen.add(arg);
11
+ if(arg==='--target'){const value=args[++i];if(!['codex','claude','all'].includes(value))throw inputError('Invalid target');options.target=value;}else options[arg==='--dry-run'?'dryRun':arg.slice(2)]=true;
12
+ }
13
+ if(options.check&&options.dryRun)throw inputError('--check and --dry-run cannot be combined');return options;
14
+ }
15
+ export async function runCli(args=process.argv.slice(2),{input=process.stdin,output=process.stdout,error=process.stderr}={}){
16
+ let onInterrupt;
17
+ try{
18
+ const options=parseArgs(args);if(options.help){output.write(usage+'\n');return 0;}
19
+ await selectChoices(options,{input,output});
20
+ const[{loadNative},{openRoot},{resolveTarget},{buildPresetPlan,switchPresetTargets}]=await Promise.all([import('../safe-fs/native.mjs'),import('../safe-fs/index.mjs'),import('../installer/targets.mjs'),import('../installer/preset.mjs')]);
21
+ const native=loadNative(),safeFs={openRoot:p=>openRoot(p,{native})};
22
+ const plans=(options.target==='all'?['codex','claude']:[options.target]).map(platform=>buildPresetPlan({target:resolveTarget(platform,{userHome:os.homedir(),env:process.env}),preset:options.preset,safeFs}));
23
+ validatePlanSet(plans);
24
+ if(options.check||options.dryRun){for(const plan of plans){output.write(`${plan.target.platform}: ${plan.specs.length} file(s) would change\n`);if(options.dryRun)for(const spec of plan.specs)output.write(`Would write: ${spec.path}\n`);}output.write('Check completed. No changes made.\n');return 0;}
25
+ const controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);const result=await switchPresetTargets(plans,{safeFs,signal:controller.signal});
26
+ for(const item of result.results){output.write(`${item.platform}: ${item.status}; rollback: ${item.rollback}\n`);for(const name of item.residualPaths)output.write(`Residual path: ${name}\n`);if(item.error)error.write(`Error: ${item.error.message}\n`);}
27
+ output.write('Open a new session to use the selected preset.\n');return result.interrupted?130:result.results.every(item=>['updated','unchanged'].includes(item.status)&&!item.error)?0:1;
28
+ }catch(cause){error.write(`Error: ${cause.message}\n`);if(cause.code==='ARC_CLI_INPUT'){error.write(usage+'\n');return 2;}return cause.code==='ARC_CANCELLED'?130:['ARC_UNSUPPORTED_RUNTIME','ARC_NATIVE_UNAVAILABLE','ARC_TARGET_CONFIGURATION'].includes(cause.code)?2:1;}finally{if(onInterrupt)process.removeListener('SIGINT',onInterrupt);}
29
+ }
@@ -0,0 +1,10 @@
1
+ export function startProgress({output,label,intervalMs=80,setInterval=globalThis.setInterval,clearInterval=globalThis.clearInterval}){
2
+ const frames=['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];let frame=0,stopped=false,timer;
3
+ const clear=()=>output.write('\r\x1b[2K');
4
+ const render=()=>output.write(`${frames[frame]} ${label}`);
5
+ if(output.isTTY){render();timer=setInterval(()=>{clear();frame=(frame+1)%frames.length;render();},intervalMs);}else output.write(label+'\n');
6
+ return{
7
+ write(text){if(output.isTTY&&!stopped){clear();output.write(text);render();}else output.write(text);},
8
+ stop(){if(stopped)return;stopped=true;if(timer!==undefined)clearInterval(timer);if(output.isTTY){clear();output.write('\n');}}
9
+ };
10
+ }
@@ -0,0 +1,20 @@
1
+ import fs from 'node:fs';
2
+ import {renderCompletion} from './completion.mjs';
3
+ const usage='Usage: arc [installation options]\n arc preset [economy|balanced|smart] --target <codex|claude|all>\n arc uninstall --target <codex|claude|all>\n arc completion <zsh|bash> (shell setup)\n arc --help | --version';
4
+ const completionUsage='Usage: arc completion <zsh|bash>';
5
+ export async function runRoot(args=process.argv.slice(2),io={}){
6
+ const output=io.output??process.stdout,error=io.error??process.stderr;
7
+ const [command,...rest]=args;
8
+ if(command==='--help'&&!rest.length){output.write(usage+'\n\n');return(await import('./index.mjs')).runCli(args,io);}
9
+ if(command==='--version'&&!rest.length){output.write(JSON.parse(fs.readFileSync(new URL('../../package.json',import.meta.url))).version+'\n');return 0;}
10
+ if(command==='preset')return(await import('./preset.mjs')).runCli(rest,io);
11
+ if(command==='install'){error.write('Error: The install subcommand was removed. Run npx --yes @yunhozz/arc [installation options].\n');return 2;}
12
+ if(command===undefined||command.startsWith('--'))return(await import('./index.mjs')).runCli(args,io);
13
+ if(command==='uninstall')return(await import('./uninstall.mjs')).runCli(rest,io);
14
+ if(command==='completion'){
15
+ if(rest.length===1&&rest[0]==='--help'){output.write(completionUsage+'\n');return 0;}
16
+ if(rest.length===1&&['zsh','bash'].includes(rest[0])){output.write(renderCompletion(rest[0]));return 0;}
17
+ error.write('Error: Expected a completion shell (zsh or bash).\n'+completionUsage+'\n');return 2;
18
+ }
19
+ error.write(`Error: Unknown command: ${command}\n${usage}\n`);return 2;
20
+ }
@@ -0,0 +1,17 @@
1
+ import fs from 'node:fs';import os from 'node:os';import path from 'node:path';import {fileURLToPath} from 'node:url';
2
+ const packageRoot=fileURLToPath(new URL('../../',import.meta.url));
3
+ const usage='Usage: arc uninstall [--target <claude|codex|all>] [--dry-run] [--skip-global] [--help] [--version]';
4
+ function inputError(message){return Object.assign(new Error(message),{code:'ARC_CLI_INPUT'});}
5
+ export function parseArgs(args){const options={},seen=new Set(),names={'--target':'target','--dry-run':'dryRun','--skip-global':'skipGlobal','--help':'help','--version':'version'};for(let i=0;i<args.length;i++){const flag=args[i],key=Object.hasOwn(names,flag)?names[flag]:undefined;if(!key)throw inputError(`Unknown argument: ${flag}`);if(seen.has(flag))throw inputError(`Duplicate argument: ${flag}`);seen.add(flag);if(key==='target'){const value=args[++i];if(!['claude','codex','all'].includes(value))throw inputError(`Invalid target: ${value??'missing value'}`);options.target=value;}else options[key]=true;}return options;}
6
+ export async function selectTarget(options,io={}){return(await import('./choices.mjs')).selectChoices(options,{...io,targetOnly:true});}
7
+ export async function runCli(args=process.argv.slice(2),{input=process.stdin,output=process.stdout,error=process.stderr}={}){
8
+ let onInterrupt;
9
+ try{const options=parseArgs(args);if(options.help){output.write(usage+'\n');return 0;}if(options.version){output.write(JSON.parse(fs.readFileSync(path.join(packageRoot,'package.json'))).version+'\n');return 0;}
10
+ if(!options.target&&(!input.isTTY||!output.isTTY))throw inputError('Non-interactive execution requires --target');await selectTarget(options,{input,output});
11
+ const [{loadNative},{openRoot},{resolveTarget},{buildUninstallPlan,uninstallTargets}]=await Promise.all([import('../safe-fs/native.mjs'),import('../safe-fs/index.mjs'),import('../installer/targets.mjs'),import('../installer/uninstall.mjs')]);const native=loadNative(),safeFs={openRoot:p=>openRoot(p,{native})};
12
+ const plans=(options.target==='all'?['codex','claude']:[options.target]).map(platform=>buildUninstallPlan({target:resolveTarget(platform,{userHome:os.homedir(),env:process.env}),safeFs,skipGlobal:options.skipGlobal}));
13
+ for(const plan of plans){output.write(`${plan.target.platform}: ${plan.target.home}\n`);for(const note of plan.summary)output.write(note+'\n');if(options.dryRun){for(const name of plan.removals)output.write(`Would remove: ${name}\n`);for(const change of plan.globalChanges)output.write(`Would remove managed block: ${change.path}\n`);}}
14
+ if(options.dryRun){output.write('Dry run completed. No changes made.\n');return 0;}
15
+ const controller=new AbortController();onInterrupt=()=>controller.abort();process.once('SIGINT',onInterrupt);const result=await uninstallTargets(plans,{safeFs,signal:controller.signal});for(const item of result.results){output.write(`${item.platform}: ${item.status}; rollback: ${item.rollback}\n`);for(const name of item.residualPaths)output.write(`Residual path: ${name}\n`);if(item.error)error.write(`Error: ${item.error.message}\n`);}return result.interrupted?130:result.results.every(item=>item.status==='uninstalled'&&!item.error)?0:1;
16
+ }catch(cause){error.write(`Error: ${cause.message}\n`);if(cause.code==='ARC_CANCELLED')return 130;if(cause.code==='ARC_CLI_INPUT'){error.write(usage+'\n');return 2;}return ['ARC_UNSUPPORTED_RUNTIME','ARC_NATIVE_UNAVAILABLE','ARC_TARGET_CONFIGURATION'].includes(cause.code)?2:1;}finally{if(onInterrupt)process.removeListener('SIGINT',onInterrupt);}
17
+ }