@izantech/lineup-cli 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/bin/lineup.mjs +4 -0
- package/dist/chunk-RQBQVCEG.js +1451 -0
- package/dist/cli.d.ts +40 -0
- package/dist/cli.js +14 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +18 -0
- package/package.json +69 -0
- package/schemas/json/host-adapter.schema.json +58 -0
- package/schemas/json/release-manifest.schema.json +30 -0
- package/schemas/json/state.schema.json +59 -0
- package/schemas/yaml/agent-output/architect.schema.json +15 -0
- package/schemas/yaml/agent-output/developer.schema.json +16 -0
- package/schemas/yaml/agent-output/documenter.schema.json +15 -0
- package/schemas/yaml/agent-output/researcher.schema.json +15 -0
- package/schemas/yaml/agent-output/reviewer.schema.json +16 -0
- package/schemas/yaml/agent-output/teacher.schema.json +15 -0
- package/schemas/yaml/tactic.schema.json +85 -0
|
@@ -0,0 +1,1451 @@
|
|
|
1
|
+
// src/cli.ts
|
|
2
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
3
|
+
import path8 from "path";
|
|
4
|
+
import process2 from "process";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
|
|
7
|
+
// src/lib/output.ts
|
|
8
|
+
function printJson(payload) {
|
|
9
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}
|
|
10
|
+
`);
|
|
11
|
+
}
|
|
12
|
+
function printTableLine(text) {
|
|
13
|
+
process.stdout.write(`${text}
|
|
14
|
+
`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/lib/operations.ts
|
|
18
|
+
import { rmSync as rmSync3 } from "fs";
|
|
19
|
+
|
|
20
|
+
// src/lib/errors.ts
|
|
21
|
+
var CliError = class extends Error {
|
|
22
|
+
code;
|
|
23
|
+
exitCode;
|
|
24
|
+
constructor(message, options) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "CliError";
|
|
27
|
+
this.code = options?.code ?? "cli_error";
|
|
28
|
+
this.exitCode = options?.exitCode ?? 1;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
function asErrorMessage(error) {
|
|
32
|
+
if (error instanceof Error) {
|
|
33
|
+
return error.message;
|
|
34
|
+
}
|
|
35
|
+
return String(error);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/lib/host-claude.ts
|
|
39
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
40
|
+
import path4 from "path";
|
|
41
|
+
|
|
42
|
+
// src/lib/constants.ts
|
|
43
|
+
var GENERATED_BANNER = "<!-- AUTO-GENERATED. Edit canonical source in .lineup-core/. -->";
|
|
44
|
+
var LINEUP_PLUGIN_NAME = "lineup";
|
|
45
|
+
var CLAUDE_LEGACY_PLUGIN = "lineup@izantech";
|
|
46
|
+
var CLAUDE_LOCAL_MARKETPLACE_NAME = "lineup-local";
|
|
47
|
+
var CLAUDE_LOCAL_PLUGIN = `${LINEUP_PLUGIN_NAME}@${CLAUDE_LOCAL_MARKETPLACE_NAME}`;
|
|
48
|
+
var SUPPORTED_HOSTS = ["claude", "codex"];
|
|
49
|
+
var CODEX_SKILL_DIRS = [
|
|
50
|
+
"lineup-kick-off",
|
|
51
|
+
"lineup-configure",
|
|
52
|
+
"lineup-explain",
|
|
53
|
+
"lineup-playbook"
|
|
54
|
+
];
|
|
55
|
+
var CODEX_REQUIRED_FILES = [
|
|
56
|
+
".agents/skills/lineup-kick-off/SKILL.md",
|
|
57
|
+
".agents/skills/lineup-kick-off/INIT.md",
|
|
58
|
+
".agents/skills/lineup-configure/SKILL.md",
|
|
59
|
+
".agents/skills/lineup-explain/SKILL.md",
|
|
60
|
+
".agents/skills/lineup-playbook/SKILL.md"
|
|
61
|
+
];
|
|
62
|
+
var HOST_TEMPLATE_SPECS = [
|
|
63
|
+
{
|
|
64
|
+
source: ".lineup-core/skills/kick-off/core.md",
|
|
65
|
+
targetFor: {
|
|
66
|
+
claude: "skills/{{SKILL_NAME_KICKOFF}}/SKILL.md",
|
|
67
|
+
codex: ".agents/skills/{{SKILL_NAME_KICKOFF}}/SKILL.md"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
source: ".lineup-core/skills/kick-off/init.core.md",
|
|
72
|
+
targetFor: {
|
|
73
|
+
claude: "skills/{{SKILL_NAME_KICKOFF}}/INIT.md",
|
|
74
|
+
codex: ".agents/skills/{{SKILL_NAME_KICKOFF}}/INIT.md"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
source: ".lineup-core/skills/configure/core.md",
|
|
79
|
+
targetFor: {
|
|
80
|
+
claude: "skills/{{SKILL_NAME_CONFIGURE}}/SKILL.md",
|
|
81
|
+
codex: ".agents/skills/{{SKILL_NAME_CONFIGURE}}/SKILL.md"
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
source: ".lineup-core/skills/explain/core.md",
|
|
86
|
+
targetFor: {
|
|
87
|
+
claude: "skills/{{SKILL_NAME_EXPLAIN}}/SKILL.md",
|
|
88
|
+
codex: ".agents/skills/{{SKILL_NAME_EXPLAIN}}/SKILL.md"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
source: ".lineup-core/skills/playbook/core.md",
|
|
93
|
+
targetFor: {
|
|
94
|
+
claude: "skills/{{SKILL_NAME_PLAYBOOK}}/SKILL.md",
|
|
95
|
+
codex: ".agents/skills/{{SKILL_NAME_PLAYBOOK}}/SKILL.md"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
// src/lib/generate.ts
|
|
101
|
+
import { cpSync, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
102
|
+
import path3 from "path";
|
|
103
|
+
|
|
104
|
+
// src/lib/validation.ts
|
|
105
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
106
|
+
import path2 from "path";
|
|
107
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
108
|
+
import addFormats from "ajv-formats";
|
|
109
|
+
import { parseDocument } from "yaml";
|
|
110
|
+
|
|
111
|
+
// src/lib/paths.ts
|
|
112
|
+
import { existsSync, readFileSync } from "fs";
|
|
113
|
+
import os from "os";
|
|
114
|
+
import path from "path";
|
|
115
|
+
import { fileURLToPath } from "url";
|
|
116
|
+
function packageRoot() {
|
|
117
|
+
const file = fileURLToPath(import.meta.url);
|
|
118
|
+
let current = path.dirname(file);
|
|
119
|
+
const filesystemRoot = path.parse(current).root;
|
|
120
|
+
while (true) {
|
|
121
|
+
const candidate = path.join(current, "package.json");
|
|
122
|
+
if (existsSync(candidate)) {
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
125
|
+
if (parsed.name === "@izantech/lineup-cli") {
|
|
126
|
+
return current;
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (current === filesystemRoot) {
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
current = path.dirname(current);
|
|
135
|
+
}
|
|
136
|
+
return path.resolve(path.dirname(file), "..", "..");
|
|
137
|
+
}
|
|
138
|
+
function lineupHome(homeDir = os.homedir()) {
|
|
139
|
+
return path.join(homeDir, ".lineup");
|
|
140
|
+
}
|
|
141
|
+
function lineupStateFile(homeDir = os.homedir()) {
|
|
142
|
+
return path.join(lineupHome(homeDir), "state.json");
|
|
143
|
+
}
|
|
144
|
+
function lineupCacheDir(homeDir = os.homedir()) {
|
|
145
|
+
return path.join(lineupHome(homeDir), "cache");
|
|
146
|
+
}
|
|
147
|
+
function codexGlobalSkillsDir(homeDir = os.homedir()) {
|
|
148
|
+
return path.join(homeDir, ".agents", "skills");
|
|
149
|
+
}
|
|
150
|
+
function claudeHostRoot(homeDir = os.homedir()) {
|
|
151
|
+
return path.join(lineupHome(homeDir), "hosts", "claude");
|
|
152
|
+
}
|
|
153
|
+
function codexHostRoot(homeDir = os.homedir()) {
|
|
154
|
+
return path.join(lineupHome(homeDir), "hosts", "codex");
|
|
155
|
+
}
|
|
156
|
+
function claudeManagedPluginDir(version, homeDir = os.homedir()) {
|
|
157
|
+
return path.join(claudeMarketplaceRoot(homeDir), "plugins", "lineup", version);
|
|
158
|
+
}
|
|
159
|
+
function claudeMarketplaceRoot(homeDir = os.homedir()) {
|
|
160
|
+
return path.join(claudeHostRoot(homeDir), "marketplace");
|
|
161
|
+
}
|
|
162
|
+
function codexRepoLocalSkillsDir(cwd = process.cwd()) {
|
|
163
|
+
return path.join(cwd, ".agents", "skills");
|
|
164
|
+
}
|
|
165
|
+
function purgeTargets(hosts, homeDir = os.homedir()) {
|
|
166
|
+
const targets = [];
|
|
167
|
+
if (hosts.includes("claude")) {
|
|
168
|
+
targets.push(path.join(homeDir, ".claude", "lineup", "agents"));
|
|
169
|
+
}
|
|
170
|
+
if (hosts.includes("codex")) {
|
|
171
|
+
targets.push(path.join(homeDir, ".codex", "lineup", "agents"));
|
|
172
|
+
targets.push(path.join(homeDir, ".codex", "lineup", "memory"));
|
|
173
|
+
}
|
|
174
|
+
return targets;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/lib/validation.ts
|
|
178
|
+
var ajv = new Ajv2020({ allErrors: true, strict: true, allowUnionTypes: true });
|
|
179
|
+
addFormats(ajv);
|
|
180
|
+
var validators = /* @__PURE__ */ new Map();
|
|
181
|
+
function schemaPath(relativePath) {
|
|
182
|
+
return path2.join(packageRoot(), "schemas", relativePath);
|
|
183
|
+
}
|
|
184
|
+
function loadValidator(relativePath) {
|
|
185
|
+
const key = relativePath;
|
|
186
|
+
const existing = validators.get(key);
|
|
187
|
+
if (existing) {
|
|
188
|
+
return existing;
|
|
189
|
+
}
|
|
190
|
+
const content = readFileSync2(schemaPath(relativePath), "utf8");
|
|
191
|
+
const schema = JSON.parse(content);
|
|
192
|
+
const compiled = ajv.compile(schema);
|
|
193
|
+
validators.set(key, compiled);
|
|
194
|
+
return compiled;
|
|
195
|
+
}
|
|
196
|
+
function formatErrors(validate) {
|
|
197
|
+
const lines = (validate.errors ?? []).map((error) => {
|
|
198
|
+
const loc = error.instancePath || "/";
|
|
199
|
+
return `- ${loc} ${error.message ?? "invalid"}`;
|
|
200
|
+
});
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
}
|
|
203
|
+
function assertValid(schemaRelPath, payload, label) {
|
|
204
|
+
const validate = loadValidator(schemaRelPath);
|
|
205
|
+
const ok = validate(payload);
|
|
206
|
+
if (!ok) {
|
|
207
|
+
throw new CliError(`${label} failed schema validation:
|
|
208
|
+
${formatErrors(validate)}`, {
|
|
209
|
+
code: "schema_validation_failed"
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function validateHostAdapter(payload, source) {
|
|
214
|
+
assertValid("json/host-adapter.schema.json", payload, `Host adapter ${source}`);
|
|
215
|
+
return payload;
|
|
216
|
+
}
|
|
217
|
+
function validateInstallerState(payload, source) {
|
|
218
|
+
assertValid("json/state.schema.json", payload, `State file ${source}`);
|
|
219
|
+
return payload;
|
|
220
|
+
}
|
|
221
|
+
function validateReleaseManifest(payload, source) {
|
|
222
|
+
assertValid("json/release-manifest.schema.json", payload, `Release manifest ${source}`);
|
|
223
|
+
return payload;
|
|
224
|
+
}
|
|
225
|
+
function parseRestrictedYaml(content, source) {
|
|
226
|
+
if (/(^|\s)&[A-Za-z0-9_-]+/m.test(content)) {
|
|
227
|
+
throw new CliError(`${source}: YAML anchors are not allowed.`, {
|
|
228
|
+
code: "yaml_anchor_not_allowed"
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
if (/(^|\s)\*[A-Za-z0-9_-]+/m.test(content)) {
|
|
232
|
+
throw new CliError(`${source}: YAML aliases are not allowed.`, {
|
|
233
|
+
code: "yaml_alias_not_allowed"
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (/(^|\s)!\S+/m.test(content)) {
|
|
237
|
+
throw new CliError(`${source}: YAML custom tags are not allowed.`, {
|
|
238
|
+
code: "yaml_tag_not_allowed"
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const doc = parseDocument(content, {
|
|
242
|
+
uniqueKeys: true,
|
|
243
|
+
merge: false
|
|
244
|
+
});
|
|
245
|
+
if (doc.errors.length > 0) {
|
|
246
|
+
const message = doc.errors.map((item) => item.message).join("\n");
|
|
247
|
+
throw new CliError(`${source}: YAML parse failed:
|
|
248
|
+
${message}`, {
|
|
249
|
+
code: "yaml_parse_failed"
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return doc.toJSON();
|
|
253
|
+
}
|
|
254
|
+
function validateTacticYaml(content, source) {
|
|
255
|
+
const parsed = parseRestrictedYaml(content, source);
|
|
256
|
+
assertValid("yaml/tactic.schema.json", parsed, `Tactic YAML ${source}`);
|
|
257
|
+
}
|
|
258
|
+
var AGENT_SCHEMA_MAP = {
|
|
259
|
+
researcher: "yaml/agent-output/researcher.schema.json",
|
|
260
|
+
architect: "yaml/agent-output/architect.schema.json",
|
|
261
|
+
developer: "yaml/agent-output/developer.schema.json",
|
|
262
|
+
reviewer: "yaml/agent-output/reviewer.schema.json",
|
|
263
|
+
documenter: "yaml/agent-output/documenter.schema.json",
|
|
264
|
+
teacher: "yaml/agent-output/teacher.schema.json"
|
|
265
|
+
};
|
|
266
|
+
function validateAgentOutputYaml(kind, content, source) {
|
|
267
|
+
const parsed = parseRestrictedYaml(content, source);
|
|
268
|
+
assertValid(AGENT_SCHEMA_MAP[kind], parsed, `${kind} output ${source}`);
|
|
269
|
+
}
|
|
270
|
+
function readJsonFile(filePath) {
|
|
271
|
+
return JSON.parse(readFileSync2(filePath, "utf8"));
|
|
272
|
+
}
|
|
273
|
+
function readTextFile(filePath) {
|
|
274
|
+
return readFileSync2(filePath, "utf8");
|
|
275
|
+
}
|
|
276
|
+
function listFilesWithExtension(directory, extension) {
|
|
277
|
+
if (!existsSync2(directory)) {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
return readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(extension)).map((entry) => path2.join(directory, entry.name)).sort();
|
|
281
|
+
}
|
|
282
|
+
function validateSourceBundle(sourceRoot) {
|
|
283
|
+
const hostsDir = path2.join(sourceRoot, ".lineup-core", "hosts");
|
|
284
|
+
for (const hostFile of listFilesWithExtension(hostsDir, ".json")) {
|
|
285
|
+
validateHostAdapter(readJsonFile(hostFile), hostFile);
|
|
286
|
+
}
|
|
287
|
+
const tacticsDir = path2.join(sourceRoot, "tactics");
|
|
288
|
+
for (const tacticFile of listFilesWithExtension(tacticsDir, ".yaml")) {
|
|
289
|
+
validateTacticYaml(readTextFile(tacticFile), tacticFile);
|
|
290
|
+
}
|
|
291
|
+
const templateMap = {
|
|
292
|
+
researcher: "researcher",
|
|
293
|
+
architect: "architect",
|
|
294
|
+
developer: "developer",
|
|
295
|
+
reviewer: "reviewer",
|
|
296
|
+
documenter: "documenter",
|
|
297
|
+
teacher: "teacher"
|
|
298
|
+
};
|
|
299
|
+
const templatesDir = path2.join(sourceRoot, "templates");
|
|
300
|
+
for (const [name, kind] of Object.entries(templateMap)) {
|
|
301
|
+
const filePath = path2.join(templatesDir, `${name}.yaml`);
|
|
302
|
+
validateAgentOutputYaml(kind, readTextFile(filePath), filePath);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/lib/generate.ts
|
|
307
|
+
function readJson(filePath) {
|
|
308
|
+
return JSON.parse(readFileSync3(filePath, "utf8"));
|
|
309
|
+
}
|
|
310
|
+
function readText(filePath) {
|
|
311
|
+
return readFileSync3(filePath, "utf8");
|
|
312
|
+
}
|
|
313
|
+
function loadHostAdapter(sourceRoot, host) {
|
|
314
|
+
const adapterPath = path3.join(sourceRoot, ".lineup-core", "hosts", `${host}.json`);
|
|
315
|
+
const payload = readJson(adapterPath);
|
|
316
|
+
return validateHostAdapter(payload, adapterPath);
|
|
317
|
+
}
|
|
318
|
+
function renderTemplate(rawTemplate, vars, source, host) {
|
|
319
|
+
return rawTemplate.replace(/\{\{([A-Z0-9_]+)\}\}/g, (full, token) => {
|
|
320
|
+
if (!(token in vars)) {
|
|
321
|
+
throw new CliError(`Missing template variable '${token}' while rendering ${source} for ${host}.`, {
|
|
322
|
+
code: "missing_template_variable"
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return String(vars[token]);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
function renderPathTemplate(pathTemplate, vars) {
|
|
329
|
+
return pathTemplate.replace(/\{\{([A-Z0-9_]+)\}\}/g, (full, token) => {
|
|
330
|
+
return vars[token] ?? full;
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
function withBanner(content) {
|
|
334
|
+
const trimmed = content.replace(/\s+$/u, "");
|
|
335
|
+
return `${GENERATED_BANNER}
|
|
336
|
+
|
|
337
|
+
${trimmed}
|
|
338
|
+
`;
|
|
339
|
+
}
|
|
340
|
+
function generateHostFiles(sourceRoot, host) {
|
|
341
|
+
const adapter = loadHostAdapter(sourceRoot, host);
|
|
342
|
+
const vars = adapter.vars;
|
|
343
|
+
return HOST_TEMPLATE_SPECS.map((spec) => {
|
|
344
|
+
const source = spec.source;
|
|
345
|
+
const template = readText(path3.join(sourceRoot, source));
|
|
346
|
+
const rendered = renderTemplate(template, vars, source, host);
|
|
347
|
+
const pathTemplate = spec.targetFor[host];
|
|
348
|
+
return {
|
|
349
|
+
host,
|
|
350
|
+
source,
|
|
351
|
+
target: renderPathTemplate(pathTemplate, vars),
|
|
352
|
+
content: withBanner(rendered)
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function writeGeneratedFiles(files, outputRoot) {
|
|
357
|
+
for (const file of files) {
|
|
358
|
+
const absolute = path3.join(outputRoot, file.target);
|
|
359
|
+
mkdirSync(path3.dirname(absolute), { recursive: true });
|
|
360
|
+
writeFileSync(absolute, file.content, "utf8");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function prepareClaudePluginSkeleton(sourceRoot, outputRoot) {
|
|
364
|
+
const copyPaths = [".claude-plugin", "agents", "tactics", "templates", "examples", "AGENTS.md", "CLAUDE.md"];
|
|
365
|
+
for (const entry of copyPaths) {
|
|
366
|
+
const from = path3.join(sourceRoot, entry);
|
|
367
|
+
const to = path3.join(outputRoot, entry);
|
|
368
|
+
cpSync(from, to, { recursive: true });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// src/lib/process.ts
|
|
373
|
+
import { spawn } from "child_process";
|
|
374
|
+
async function runCommand(command, args) {
|
|
375
|
+
return new Promise((resolve, reject) => {
|
|
376
|
+
const child = spawn(command, args, {
|
|
377
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
378
|
+
env: process.env
|
|
379
|
+
});
|
|
380
|
+
let stdout = "";
|
|
381
|
+
let stderr = "";
|
|
382
|
+
child.stdout.on("data", (chunk) => {
|
|
383
|
+
stdout += chunk.toString();
|
|
384
|
+
});
|
|
385
|
+
child.stderr.on("data", (chunk) => {
|
|
386
|
+
stderr += chunk.toString();
|
|
387
|
+
});
|
|
388
|
+
child.on("error", (error) => {
|
|
389
|
+
if (error.code === "ENOENT") {
|
|
390
|
+
reject(
|
|
391
|
+
new CliError(`Required command not found: ${command}`, {
|
|
392
|
+
code: "command_not_found"
|
|
393
|
+
})
|
|
394
|
+
);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
reject(error);
|
|
398
|
+
});
|
|
399
|
+
child.on("close", (code) => {
|
|
400
|
+
resolve({
|
|
401
|
+
code: code ?? 1,
|
|
402
|
+
stdout,
|
|
403
|
+
stderr
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
function assertSuccess(result, label, allowPatterns = []) {
|
|
409
|
+
if (result.code === 0) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const combined = `${result.stdout}
|
|
413
|
+
${result.stderr}`;
|
|
414
|
+
if (allowPatterns.some((pattern) => pattern.test(combined))) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
throw new CliError(
|
|
418
|
+
[
|
|
419
|
+
`${label} failed with exit code ${result.code}.`,
|
|
420
|
+
result.stdout.trim() ? `stdout:
|
|
421
|
+
${result.stdout.trim()}` : null,
|
|
422
|
+
result.stderr.trim() ? `stderr:
|
|
423
|
+
${result.stderr.trim()}` : null
|
|
424
|
+
].filter(Boolean).join("\n"),
|
|
425
|
+
{
|
|
426
|
+
code: "command_failed"
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/lib/host-claude.ts
|
|
432
|
+
function parseInstallPresence(output2) {
|
|
433
|
+
return {
|
|
434
|
+
localInstalled: new RegExp(`${LINEUP_PLUGIN_NAME}@${CLAUDE_LOCAL_MARKETPLACE_NAME}`, "i").test(output2),
|
|
435
|
+
legacyInstalled: new RegExp(CLAUDE_LEGACY_PLUGIN, "i").test(output2)
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function writeMarketplace(root, pluginSource, version) {
|
|
439
|
+
const dotClaude = path4.join(root, ".claude-plugin");
|
|
440
|
+
mkdirSync2(dotClaude, { recursive: true });
|
|
441
|
+
const marketplace = {
|
|
442
|
+
name: CLAUDE_LOCAL_MARKETPLACE_NAME,
|
|
443
|
+
owner: {
|
|
444
|
+
name: "izantech"
|
|
445
|
+
},
|
|
446
|
+
metadata: {
|
|
447
|
+
description: "CLI-managed local marketplace for Lineup",
|
|
448
|
+
version
|
|
449
|
+
},
|
|
450
|
+
plugins: [
|
|
451
|
+
{
|
|
452
|
+
name: LINEUP_PLUGIN_NAME,
|
|
453
|
+
source: `./${path4.relative(root, pluginSource)}`,
|
|
454
|
+
version,
|
|
455
|
+
description: "Lineup local managed plugin"
|
|
456
|
+
}
|
|
457
|
+
]
|
|
458
|
+
};
|
|
459
|
+
const filePath = path4.join(dotClaude, "marketplace.json");
|
|
460
|
+
writeFileSync2(filePath, `${JSON.stringify(marketplace, null, 2)}
|
|
461
|
+
`, "utf8");
|
|
462
|
+
return root;
|
|
463
|
+
}
|
|
464
|
+
function prepareClaudePluginFromSource(sourceRoot, version) {
|
|
465
|
+
const targetRoot = claudeManagedPluginDir(version);
|
|
466
|
+
mkdirSync2(targetRoot, { recursive: true });
|
|
467
|
+
prepareClaudePluginSkeleton(sourceRoot, targetRoot);
|
|
468
|
+
const files = generateHostFiles(sourceRoot, "claude");
|
|
469
|
+
writeGeneratedFiles(files, targetRoot);
|
|
470
|
+
const pluginManifest = path4.join(targetRoot, ".claude-plugin", "plugin.json");
|
|
471
|
+
if (!existsSync3(pluginManifest)) {
|
|
472
|
+
throw new CliError(`Generated Claude plugin is missing manifest: ${pluginManifest}`, {
|
|
473
|
+
code: "missing_plugin_manifest"
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return targetRoot;
|
|
477
|
+
}
|
|
478
|
+
async function statusClaude() {
|
|
479
|
+
try {
|
|
480
|
+
const result = await runCommand("claude", ["plugin", "list"]);
|
|
481
|
+
if (result.code !== 0) {
|
|
482
|
+
return {
|
|
483
|
+
host: "claude",
|
|
484
|
+
installed: false,
|
|
485
|
+
version: null,
|
|
486
|
+
source: null,
|
|
487
|
+
last_action: null,
|
|
488
|
+
error: result.stderr.trim() || "Failed to run claude plugin list"
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const output2 = `${result.stdout}
|
|
492
|
+
${result.stderr}`;
|
|
493
|
+
const parsed = parseInstallPresence(output2);
|
|
494
|
+
return {
|
|
495
|
+
host: "claude",
|
|
496
|
+
installed: parsed.localInstalled || parsed.legacyInstalled,
|
|
497
|
+
version: null,
|
|
498
|
+
source: parsed.localInstalled ? "cli-managed" : parsed.legacyInstalled ? "legacy-marketplace" : null,
|
|
499
|
+
last_action: null,
|
|
500
|
+
...parsed.legacyInstalled ? { error: "Legacy marketplace install detected." } : {}
|
|
501
|
+
};
|
|
502
|
+
} catch (error) {
|
|
503
|
+
return {
|
|
504
|
+
host: "claude",
|
|
505
|
+
installed: false,
|
|
506
|
+
version: null,
|
|
507
|
+
source: null,
|
|
508
|
+
last_action: null,
|
|
509
|
+
error: error instanceof Error ? error.message : String(error)
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async function installClaudeFromPreparedPlugin({
|
|
514
|
+
pluginSource,
|
|
515
|
+
version,
|
|
516
|
+
migrateLegacy
|
|
517
|
+
}) {
|
|
518
|
+
const marketplaceRoot = claudeMarketplaceRoot();
|
|
519
|
+
const marketplacePath = writeMarketplace(marketplaceRoot, pluginSource, version);
|
|
520
|
+
if (migrateLegacy) {
|
|
521
|
+
const removeLegacy = await runCommand("claude", ["plugin", "remove", CLAUDE_LEGACY_PLUGIN]);
|
|
522
|
+
if (removeLegacy.code !== 0) {
|
|
523
|
+
const combined = `${removeLegacy.stdout}
|
|
524
|
+
${removeLegacy.stderr}`;
|
|
525
|
+
if (!/not installed|unknown|could not find/i.test(combined)) {
|
|
526
|
+
assertSuccess(removeLegacy, "claude plugin remove lineup@izantech");
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const addMarketplace = await runCommand("claude", ["plugin", "marketplace", "add", marketplacePath]);
|
|
531
|
+
assertSuccess(addMarketplace, "claude plugin marketplace add", [/already|exists|configured/i]);
|
|
532
|
+
const installPlugin = await runCommand("claude", ["plugin", "install", CLAUDE_LOCAL_PLUGIN]);
|
|
533
|
+
assertSuccess(installPlugin, `claude plugin install ${CLAUDE_LOCAL_PLUGIN}`, [/already installed|already exists/i]);
|
|
534
|
+
}
|
|
535
|
+
async function updateClaudeLocal() {
|
|
536
|
+
const update = await runCommand("claude", ["plugin", "update", CLAUDE_LOCAL_PLUGIN]);
|
|
537
|
+
assertSuccess(update, `claude plugin update ${CLAUDE_LOCAL_PLUGIN}`);
|
|
538
|
+
}
|
|
539
|
+
async function uninstallClaude() {
|
|
540
|
+
const removeLocal = await runCommand("claude", ["plugin", "remove", CLAUDE_LOCAL_PLUGIN]);
|
|
541
|
+
if (removeLocal.code !== 0) {
|
|
542
|
+
const output2 = `${removeLocal.stdout}
|
|
543
|
+
${removeLocal.stderr}`;
|
|
544
|
+
if (!/not installed|unknown/i.test(output2)) {
|
|
545
|
+
assertSuccess(removeLocal, `claude plugin remove ${CLAUDE_LOCAL_PLUGIN}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
async function detectLegacyClaudeInstall() {
|
|
550
|
+
const status = await statusClaude();
|
|
551
|
+
return status.source === "legacy-marketplace";
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/lib/host-codex.ts
|
|
555
|
+
import { cpSync as cpSync2, existsSync as existsSync4, mkdirSync as mkdirSync3, renameSync, rmSync } from "fs";
|
|
556
|
+
import path5 from "path";
|
|
557
|
+
function ensureCodexGenerated(sourceRoot, outputRoot) {
|
|
558
|
+
const files = generateHostFiles(sourceRoot, "codex");
|
|
559
|
+
writeGeneratedFiles(files, outputRoot);
|
|
560
|
+
return path5.join(outputRoot, ".agents", "skills");
|
|
561
|
+
}
|
|
562
|
+
function requiredAbsolutePaths(baseDir) {
|
|
563
|
+
return CODEX_REQUIRED_FILES.map((relative) => path5.join(baseDir, ...relative.replace(/^\.agents\/skills\//, "").split("/")));
|
|
564
|
+
}
|
|
565
|
+
function validateCodexSkillsDir(baseDir) {
|
|
566
|
+
const missing = requiredAbsolutePaths(baseDir).filter((item) => !existsSync4(item));
|
|
567
|
+
if (missing.length > 0) {
|
|
568
|
+
throw new CliError([`Codex skills directory is missing required files in ${baseDir}:`, ...missing.map((item) => `- ${item}`)].join("\n"), {
|
|
569
|
+
code: "codex_skill_validation_failed"
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function replaceDirectoryAtomic(sourceDir, targetDir) {
|
|
574
|
+
const parentDir = path5.dirname(targetDir);
|
|
575
|
+
const nonce = `${Date.now()}-${process.pid}`;
|
|
576
|
+
const tempDir = path5.join(parentDir, `.${path5.basename(targetDir)}.tmp-${nonce}`);
|
|
577
|
+
const backupDir = path5.join(parentDir, `.${path5.basename(targetDir)}.bak-${nonce}`);
|
|
578
|
+
mkdirSync3(parentDir, { recursive: true });
|
|
579
|
+
cpSync2(sourceDir, tempDir, { recursive: true });
|
|
580
|
+
let movedTarget = false;
|
|
581
|
+
try {
|
|
582
|
+
if (existsSync4(targetDir)) {
|
|
583
|
+
renameSync(targetDir, backupDir);
|
|
584
|
+
movedTarget = true;
|
|
585
|
+
}
|
|
586
|
+
renameSync(tempDir, targetDir);
|
|
587
|
+
if (movedTarget && existsSync4(backupDir)) {
|
|
588
|
+
rmSync(backupDir, { recursive: true, force: true });
|
|
589
|
+
}
|
|
590
|
+
} catch (error) {
|
|
591
|
+
if (existsSync4(tempDir)) {
|
|
592
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
593
|
+
}
|
|
594
|
+
if (movedTarget && !existsSync4(targetDir) && existsSync4(backupDir)) {
|
|
595
|
+
renameSync(backupDir, targetDir);
|
|
596
|
+
}
|
|
597
|
+
throw error;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function installCodex({
|
|
601
|
+
sourceRoot,
|
|
602
|
+
workspaceRoot,
|
|
603
|
+
global = true
|
|
604
|
+
}) {
|
|
605
|
+
const generatedRoot = path5.join(workspaceRoot, "generated", "codex");
|
|
606
|
+
const sourceSkills = ensureCodexGenerated(sourceRoot, generatedRoot);
|
|
607
|
+
validateCodexSkillsDir(sourceSkills);
|
|
608
|
+
const destinationRoot = global ? codexGlobalSkillsDir() : codexRepoLocalSkillsDir();
|
|
609
|
+
mkdirSync3(destinationRoot, { recursive: true });
|
|
610
|
+
for (const dirName of CODEX_SKILL_DIRS) {
|
|
611
|
+
const from = path5.join(sourceSkills, dirName);
|
|
612
|
+
const to = path5.join(destinationRoot, dirName);
|
|
613
|
+
replaceDirectoryAtomic(from, to);
|
|
614
|
+
}
|
|
615
|
+
validateCodexSkillsDir(destinationRoot);
|
|
616
|
+
return {
|
|
617
|
+
skills_dir: destinationRoot,
|
|
618
|
+
files_verified: CODEX_REQUIRED_FILES.length
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
function uninstallCodex(global = true) {
|
|
622
|
+
const root = global ? codexGlobalSkillsDir() : codexRepoLocalSkillsDir();
|
|
623
|
+
for (const dirName of CODEX_SKILL_DIRS) {
|
|
624
|
+
const target = path5.join(root, dirName);
|
|
625
|
+
if (existsSync4(target)) {
|
|
626
|
+
rmSync(target, { recursive: true, force: true });
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return { skills_dir: root };
|
|
630
|
+
}
|
|
631
|
+
function statusCodex(global = true) {
|
|
632
|
+
const root = global ? codexGlobalSkillsDir() : codexRepoLocalSkillsDir();
|
|
633
|
+
const missing = requiredAbsolutePaths(root).filter((item) => !existsSync4(item));
|
|
634
|
+
return {
|
|
635
|
+
host: "codex",
|
|
636
|
+
installed: missing.length === 0,
|
|
637
|
+
version: null,
|
|
638
|
+
source: null,
|
|
639
|
+
last_action: null,
|
|
640
|
+
...missing.length > 0 ? { error: `Missing ${missing.length} required files.` } : {}
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/lib/prompts.ts
|
|
645
|
+
import readline from "readline/promises";
|
|
646
|
+
import { stdin as input, stdout as output } from "process";
|
|
647
|
+
function isInteractive() {
|
|
648
|
+
return Boolean(input.isTTY && output.isTTY);
|
|
649
|
+
}
|
|
650
|
+
function createInterface() {
|
|
651
|
+
return readline.createInterface({ input, output });
|
|
652
|
+
}
|
|
653
|
+
function parseYesNo(raw, defaultValue) {
|
|
654
|
+
const value = raw.trim().toLowerCase();
|
|
655
|
+
if (!value) {
|
|
656
|
+
return defaultValue;
|
|
657
|
+
}
|
|
658
|
+
if (value === "y" || value === "yes") {
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
if (value === "n" || value === "no") {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
async function promptHostSelection() {
|
|
667
|
+
const rl = createInterface();
|
|
668
|
+
try {
|
|
669
|
+
output.write("Select host(s):\n");
|
|
670
|
+
output.write(" 1. claude\n");
|
|
671
|
+
output.write(" 2. codex\n");
|
|
672
|
+
output.write(" 3. all\n");
|
|
673
|
+
while (true) {
|
|
674
|
+
const answer = await rl.question("Enter selection [1-3]: ");
|
|
675
|
+
const normalized = answer.trim().toLowerCase();
|
|
676
|
+
if (normalized === "1" || normalized === "claude") {
|
|
677
|
+
return ["claude"];
|
|
678
|
+
}
|
|
679
|
+
if (normalized === "2" || normalized === "codex") {
|
|
680
|
+
return ["codex"];
|
|
681
|
+
}
|
|
682
|
+
if (normalized === "3" || normalized === "all") {
|
|
683
|
+
return ["claude", "codex"];
|
|
684
|
+
}
|
|
685
|
+
output.write("Invalid selection. Choose 1, 2, or 3.\n");
|
|
686
|
+
}
|
|
687
|
+
} finally {
|
|
688
|
+
rl.close();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
async function promptConfirm(message, defaultValue = false) {
|
|
692
|
+
const rl = createInterface();
|
|
693
|
+
try {
|
|
694
|
+
while (true) {
|
|
695
|
+
const suffix = defaultValue ? "[Y/n]" : "[y/N]";
|
|
696
|
+
const answer = await rl.question(`${message} ${suffix} `);
|
|
697
|
+
const parsed = parseYesNo(answer, defaultValue);
|
|
698
|
+
if (parsed === null) {
|
|
699
|
+
output.write("Please answer yes or no.\n");
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
return parsed;
|
|
703
|
+
}
|
|
704
|
+
} finally {
|
|
705
|
+
rl.close();
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
async function promptMigrationConfirm() {
|
|
709
|
+
return promptConfirm("Detected existing lineup@izantech install. Migrate to CLI-managed install now?", true);
|
|
710
|
+
}
|
|
711
|
+
async function promptUninstallPlan(hosts) {
|
|
712
|
+
const proceed = await promptConfirm(`Uninstall Lineup for host(s): ${hosts.join(", ")}?`, false);
|
|
713
|
+
if (!proceed) {
|
|
714
|
+
return { proceed: false, purge: false };
|
|
715
|
+
}
|
|
716
|
+
const purge = await promptConfirm(
|
|
717
|
+
"Also purge Lineup data (~/.claude/lineup/agents, ~/.codex/lineup/agents, ~/.codex/lineup/memory)?",
|
|
718
|
+
false
|
|
719
|
+
);
|
|
720
|
+
return { proceed: true, purge };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// src/lib/release.ts
|
|
724
|
+
import { createHash } from "crypto";
|
|
725
|
+
import {
|
|
726
|
+
existsSync as existsSync5,
|
|
727
|
+
mkdirSync as mkdirSync4,
|
|
728
|
+
readdirSync as readdirSync2,
|
|
729
|
+
readFileSync as readFileSync4,
|
|
730
|
+
renameSync as renameSync2,
|
|
731
|
+
rmSync as rmSync2,
|
|
732
|
+
writeFileSync as writeFileSync3
|
|
733
|
+
} from "fs";
|
|
734
|
+
import { readFile } from "fs/promises";
|
|
735
|
+
import path6 from "path";
|
|
736
|
+
var OWNER = "izantech";
|
|
737
|
+
var REPO = "lineup";
|
|
738
|
+
var API_BASE = `https://api.github.com/repos/${OWNER}/${REPO}`;
|
|
739
|
+
var MANIFEST_ASSET_NAME = "lineup-manifest.json";
|
|
740
|
+
async function fetchJson(url) {
|
|
741
|
+
const response = await fetch(url, {
|
|
742
|
+
headers: {
|
|
743
|
+
"User-Agent": "lineup-cli",
|
|
744
|
+
Accept: "application/vnd.github+json"
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
if (!response.ok) {
|
|
748
|
+
const body = await response.text();
|
|
749
|
+
throw new CliError(`Failed to fetch ${url}: ${response.status} ${body}`, {
|
|
750
|
+
code: "http_error"
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
return response.json();
|
|
754
|
+
}
|
|
755
|
+
async function fetchText(url) {
|
|
756
|
+
const response = await fetch(url, {
|
|
757
|
+
headers: {
|
|
758
|
+
"User-Agent": "lineup-cli",
|
|
759
|
+
Accept: "application/json"
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
if (!response.ok) {
|
|
763
|
+
const body = await response.text();
|
|
764
|
+
throw new CliError(`Failed to fetch ${url}: ${response.status} ${body}`, {
|
|
765
|
+
code: "http_error"
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
return response.text();
|
|
769
|
+
}
|
|
770
|
+
async function downloadBinary(url) {
|
|
771
|
+
const response = await fetch(url, {
|
|
772
|
+
headers: {
|
|
773
|
+
"User-Agent": "lineup-cli",
|
|
774
|
+
Accept: "application/octet-stream"
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
if (!response.ok) {
|
|
778
|
+
const body = await response.text();
|
|
779
|
+
throw new CliError(`Failed to download ${url}: ${response.status} ${body}`, {
|
|
780
|
+
code: "http_error"
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
784
|
+
return Buffer.from(arrayBuffer);
|
|
785
|
+
}
|
|
786
|
+
function cachePaths(tag) {
|
|
787
|
+
const cacheDir = path6.join(lineupCacheDir(), tag);
|
|
788
|
+
return {
|
|
789
|
+
cacheDir,
|
|
790
|
+
manifestPath: path6.join(cacheDir, "manifest.json"),
|
|
791
|
+
tarballPath: path6.join(cacheDir, "release.tar.gz"),
|
|
792
|
+
extractDir: path6.join(cacheDir, "extracted"),
|
|
793
|
+
sourceRoot: path6.join(cacheDir, "source")
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function sha256File(filePath) {
|
|
797
|
+
const content = readFileSync4(filePath);
|
|
798
|
+
return createHash("sha256").update(content).digest("hex");
|
|
799
|
+
}
|
|
800
|
+
async function resolveLatestTag() {
|
|
801
|
+
const payload = await fetchJson(`${API_BASE}/releases/latest`);
|
|
802
|
+
if (!payload.tag_name) {
|
|
803
|
+
throw new CliError("Unable to resolve latest release tag.", {
|
|
804
|
+
code: "missing_latest_tag"
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
return payload.tag_name;
|
|
808
|
+
}
|
|
809
|
+
async function resolveReleaseByTag(tag) {
|
|
810
|
+
const payload = await fetchJson(`${API_BASE}/releases/tags/${encodeURIComponent(tag)}`);
|
|
811
|
+
if (!payload.tag_name) {
|
|
812
|
+
throw new CliError(`Release tag not found: ${tag}`, {
|
|
813
|
+
code: "release_not_found"
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
return payload;
|
|
817
|
+
}
|
|
818
|
+
async function fetchReleaseManifest(tag) {
|
|
819
|
+
const release = await resolveReleaseByTag(tag);
|
|
820
|
+
const manifestAsset = (release.assets ?? []).find((asset) => asset.name === MANIFEST_ASSET_NAME);
|
|
821
|
+
const candidates = [];
|
|
822
|
+
if (manifestAsset?.browser_download_url) {
|
|
823
|
+
candidates.push(manifestAsset.browser_download_url);
|
|
824
|
+
}
|
|
825
|
+
candidates.push(`https://raw.githubusercontent.com/${OWNER}/${REPO}/${encodeURIComponent(tag)}/release-manifest.json`);
|
|
826
|
+
for (const url of candidates) {
|
|
827
|
+
try {
|
|
828
|
+
const text = await fetchText(url);
|
|
829
|
+
const parsed = JSON.parse(text);
|
|
830
|
+
const manifest = validateReleaseManifest(parsed, url);
|
|
831
|
+
if (manifest.tag !== tag) {
|
|
832
|
+
throw new CliError(`Manifest tag mismatch. expected=${tag} actual=${manifest.tag}`, {
|
|
833
|
+
code: "manifest_tag_mismatch"
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
return manifest;
|
|
837
|
+
} catch {
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
throw new CliError(`Could not fetch release manifest for ${tag}. Expected asset ${MANIFEST_ASSET_NAME} in release artifacts.`, {
|
|
842
|
+
code: "release_manifest_missing"
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
function validateExtractedSource(sourceRoot) {
|
|
846
|
+
const required = [
|
|
847
|
+
".lineup-core/skills/kick-off/core.md",
|
|
848
|
+
".lineup-core/hosts/claude.json",
|
|
849
|
+
".lineup-core/hosts/codex.json",
|
|
850
|
+
"agents/researcher.md",
|
|
851
|
+
"templates/tactic.yaml"
|
|
852
|
+
];
|
|
853
|
+
const missing = required.filter((item) => !existsSync5(path6.join(sourceRoot, item)));
|
|
854
|
+
if (missing.length > 0) {
|
|
855
|
+
throw new CliError(`Release source missing required files:
|
|
856
|
+
${missing.map((item) => `- ${item}`).join("\n")}`, {
|
|
857
|
+
code: "release_source_invalid"
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
function chooseExtractedRoot(extractDir) {
|
|
862
|
+
const dirs = readdirSync2(extractDir, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
|
863
|
+
if (dirs.length === 0) {
|
|
864
|
+
throw new CliError(`No extracted source directory found in ${extractDir}.`, {
|
|
865
|
+
code: "extract_failed"
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
return path6.join(extractDir, dirs[0].name);
|
|
869
|
+
}
|
|
870
|
+
async function extractTarball(tarballPath, extractDir) {
|
|
871
|
+
rmSync2(extractDir, { recursive: true, force: true });
|
|
872
|
+
mkdirSync4(extractDir, { recursive: true });
|
|
873
|
+
const result = await runCommand("tar", ["-xzf", tarballPath, "-C", extractDir]);
|
|
874
|
+
assertSuccess(result, `tar -xzf ${tarballPath}`);
|
|
875
|
+
}
|
|
876
|
+
function loadCachedManifest(manifestPath) {
|
|
877
|
+
if (!existsSync5(manifestPath)) {
|
|
878
|
+
return null;
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
const parsed = JSON.parse(readFileSync4(manifestPath, "utf8"));
|
|
882
|
+
return validateReleaseManifest(parsed, manifestPath);
|
|
883
|
+
} catch {
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
async function resolveRelease(input2 = {}) {
|
|
888
|
+
const tag = input2.version && input2.version !== "latest" ? input2.version : await resolveLatestTag();
|
|
889
|
+
const { cacheDir, manifestPath, tarballPath, extractDir, sourceRoot } = cachePaths(tag);
|
|
890
|
+
mkdirSync4(cacheDir, { recursive: true });
|
|
891
|
+
const cachedManifest = loadCachedManifest(manifestPath);
|
|
892
|
+
if (cachedManifest && existsSync5(sourceRoot)) {
|
|
893
|
+
validateExtractedSource(sourceRoot);
|
|
894
|
+
return {
|
|
895
|
+
tag,
|
|
896
|
+
sourceRoot,
|
|
897
|
+
cacheDir,
|
|
898
|
+
manifest: cachedManifest
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
const manifest = await fetchReleaseManifest(tag);
|
|
902
|
+
writeFileSync3(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
903
|
+
`, "utf8");
|
|
904
|
+
const tarball = await downloadBinary(manifest.tarball_url);
|
|
905
|
+
writeFileSync3(tarballPath, tarball);
|
|
906
|
+
const digest = sha256File(tarballPath);
|
|
907
|
+
if (digest.toLowerCase() !== manifest.sha256.toLowerCase()) {
|
|
908
|
+
rmSync2(tarballPath, { force: true });
|
|
909
|
+
throw new CliError(`Checksum mismatch for ${tag}. expected=${manifest.sha256} actual=${digest}`, {
|
|
910
|
+
code: "checksum_mismatch"
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
await extractTarball(tarballPath, extractDir);
|
|
914
|
+
const extractedRoot = chooseExtractedRoot(extractDir);
|
|
915
|
+
rmSync2(sourceRoot, { recursive: true, force: true });
|
|
916
|
+
renameSync2(extractedRoot, sourceRoot);
|
|
917
|
+
rmSync2(extractDir, { recursive: true, force: true });
|
|
918
|
+
validateExtractedSource(sourceRoot);
|
|
919
|
+
return {
|
|
920
|
+
tag,
|
|
921
|
+
sourceRoot,
|
|
922
|
+
cacheDir,
|
|
923
|
+
manifest
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
function resolveLocalRelease(dirPath) {
|
|
927
|
+
const resolvedPath = path6.resolve(dirPath);
|
|
928
|
+
if (!existsSync5(resolvedPath)) {
|
|
929
|
+
throw new CliError(`Local directory does not exist: ${resolvedPath}`, {
|
|
930
|
+
code: "local_dir_not_found"
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
validateExtractedSource(resolvedPath);
|
|
934
|
+
let tag = "local";
|
|
935
|
+
try {
|
|
936
|
+
const pkgPath = path6.join(resolvedPath, "cli", "package.json");
|
|
937
|
+
const parsed = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
938
|
+
if (parsed.version) {
|
|
939
|
+
tag = parsed.version;
|
|
940
|
+
}
|
|
941
|
+
} catch {
|
|
942
|
+
}
|
|
943
|
+
return {
|
|
944
|
+
tag,
|
|
945
|
+
sourceRoot: resolvedPath,
|
|
946
|
+
cacheDir: resolvedPath,
|
|
947
|
+
manifest: {
|
|
948
|
+
tag,
|
|
949
|
+
tarball_url: "local",
|
|
950
|
+
sha256: "local"
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// src/lib/state.ts
|
|
956
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
957
|
+
import path7 from "path";
|
|
958
|
+
var STATE_SCHEMA_VERSION = 1;
|
|
959
|
+
function defaultState() {
|
|
960
|
+
return {
|
|
961
|
+
schema_version: STATE_SCHEMA_VERSION,
|
|
962
|
+
updated_at: null,
|
|
963
|
+
hosts: {}
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
function loadState(filePath = lineupStateFile()) {
|
|
967
|
+
if (!existsSync6(filePath)) {
|
|
968
|
+
return defaultState();
|
|
969
|
+
}
|
|
970
|
+
try {
|
|
971
|
+
const raw = readFileSync5(filePath, "utf8");
|
|
972
|
+
const parsed = JSON.parse(raw);
|
|
973
|
+
return validateInstallerState(parsed, filePath);
|
|
974
|
+
} catch {
|
|
975
|
+
return defaultState();
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function saveState(state, filePath = lineupStateFile()) {
|
|
979
|
+
const payload = {
|
|
980
|
+
schema_version: STATE_SCHEMA_VERSION,
|
|
981
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
982
|
+
hosts: state.hosts
|
|
983
|
+
};
|
|
984
|
+
const valid = validateInstallerState(payload, filePath);
|
|
985
|
+
mkdirSync5(path7.dirname(filePath), { recursive: true });
|
|
986
|
+
writeFileSync4(filePath, `${JSON.stringify(valid, null, 2)}
|
|
987
|
+
`, "utf8");
|
|
988
|
+
return valid;
|
|
989
|
+
}
|
|
990
|
+
function updateHostState(state, host, patch) {
|
|
991
|
+
const previous = state.hosts[host] ?? {
|
|
992
|
+
installed: false,
|
|
993
|
+
last_action: null,
|
|
994
|
+
last_updated_at: null
|
|
995
|
+
};
|
|
996
|
+
state.hosts[host] = {
|
|
997
|
+
...previous,
|
|
998
|
+
...patch,
|
|
999
|
+
last_updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1000
|
+
};
|
|
1001
|
+
return state;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/lib/operations.ts
|
|
1005
|
+
var defaultDeps = {
|
|
1006
|
+
resolveRelease,
|
|
1007
|
+
resolveLocalRelease,
|
|
1008
|
+
validateSourceBundle,
|
|
1009
|
+
detectLegacyClaudeInstall,
|
|
1010
|
+
installClaudeFromPreparedPlugin,
|
|
1011
|
+
prepareClaudePluginFromSource,
|
|
1012
|
+
statusClaude,
|
|
1013
|
+
uninstallClaude,
|
|
1014
|
+
updateClaudeLocal,
|
|
1015
|
+
installCodex,
|
|
1016
|
+
statusCodex,
|
|
1017
|
+
uninstallCodex,
|
|
1018
|
+
codexHostRoot,
|
|
1019
|
+
lineupStateFile,
|
|
1020
|
+
purgeTargets,
|
|
1021
|
+
isInteractive,
|
|
1022
|
+
promptMigrationConfirm,
|
|
1023
|
+
promptUninstallPlan,
|
|
1024
|
+
loadState,
|
|
1025
|
+
saveState,
|
|
1026
|
+
updateHostState,
|
|
1027
|
+
removePath: (target) => {
|
|
1028
|
+
rmSync3(target, { recursive: true, force: true });
|
|
1029
|
+
},
|
|
1030
|
+
asErrorMessage
|
|
1031
|
+
};
|
|
1032
|
+
function summarizeFailures(failures, action) {
|
|
1033
|
+
const lines = [
|
|
1034
|
+
`Failed to ${action} Lineup for ${failures.length} host(s):`,
|
|
1035
|
+
...failures.map((failure) => `- ${failure.host}: ${failure.error}`)
|
|
1036
|
+
];
|
|
1037
|
+
throw new CliError(lines.join("\n"), {
|
|
1038
|
+
code: `host_${action}_failed`
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
function mergeStatus(stateHost, runtimeHost) {
|
|
1042
|
+
return {
|
|
1043
|
+
host: runtimeHost.host,
|
|
1044
|
+
installed: runtimeHost.installed,
|
|
1045
|
+
version: runtimeHost.version ?? stateHost?.version ?? null,
|
|
1046
|
+
source: runtimeHost.source ?? stateHost?.source ?? null,
|
|
1047
|
+
last_action: stateHost?.last_action ?? runtimeHost.last_action ?? null,
|
|
1048
|
+
...runtimeHost.error ? { error: runtimeHost.error } : {}
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
function createOperations(overrides = {}) {
|
|
1052
|
+
const deps = {
|
|
1053
|
+
...defaultDeps,
|
|
1054
|
+
...overrides
|
|
1055
|
+
};
|
|
1056
|
+
async function shouldMigrateLegacyClaudeInstall(yes) {
|
|
1057
|
+
const legacyDetected = await deps.detectLegacyClaudeInstall();
|
|
1058
|
+
if (!legacyDetected) {
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
if (yes) {
|
|
1062
|
+
return true;
|
|
1063
|
+
}
|
|
1064
|
+
if (!deps.isInteractive()) {
|
|
1065
|
+
throw new CliError(
|
|
1066
|
+
"Detected legacy Claude install (lineup@izantech). Re-run with --yes to migrate in non-interactive mode.",
|
|
1067
|
+
{
|
|
1068
|
+
code: "migration_confirmation_required"
|
|
1069
|
+
}
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
const approved = await deps.promptMigrationConfirm();
|
|
1073
|
+
if (!approved) {
|
|
1074
|
+
throw new CliError("Migration cancelled by user.", {
|
|
1075
|
+
code: "migration_cancelled"
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
return true;
|
|
1079
|
+
}
|
|
1080
|
+
async function performInstallOrUpdate2(input2) {
|
|
1081
|
+
const release = input2.fromDir ? deps.resolveLocalRelease(input2.fromDir) : await deps.resolveRelease({ version: input2.version });
|
|
1082
|
+
deps.validateSourceBundle(release.sourceRoot);
|
|
1083
|
+
const state = deps.loadState();
|
|
1084
|
+
const failures = [];
|
|
1085
|
+
const results = [];
|
|
1086
|
+
const migrateLegacyClaude = input2.hosts.includes("claude") ? await shouldMigrateLegacyClaudeInstall(input2.yes) : false;
|
|
1087
|
+
for (const host of input2.hosts) {
|
|
1088
|
+
try {
|
|
1089
|
+
if (host === "claude") {
|
|
1090
|
+
const pluginSource = deps.prepareClaudePluginFromSource(release.sourceRoot, release.tag);
|
|
1091
|
+
await deps.installClaudeFromPreparedPlugin({
|
|
1092
|
+
pluginSource,
|
|
1093
|
+
version: release.tag,
|
|
1094
|
+
migrateLegacy: migrateLegacyClaude
|
|
1095
|
+
});
|
|
1096
|
+
if (input2.action === "update") {
|
|
1097
|
+
await deps.updateClaudeLocal();
|
|
1098
|
+
}
|
|
1099
|
+
deps.updateHostState(state, "claude", {
|
|
1100
|
+
installed: true,
|
|
1101
|
+
version: release.tag,
|
|
1102
|
+
source: "cli-managed",
|
|
1103
|
+
last_action: input2.action
|
|
1104
|
+
});
|
|
1105
|
+
results.push({
|
|
1106
|
+
host,
|
|
1107
|
+
ok: true,
|
|
1108
|
+
message: `Claude ${input2.action} complete (${release.tag}).`
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
if (host === "codex") {
|
|
1112
|
+
const codexResult = deps.installCodex({
|
|
1113
|
+
sourceRoot: release.sourceRoot,
|
|
1114
|
+
workspaceRoot: deps.codexHostRoot(),
|
|
1115
|
+
global: true
|
|
1116
|
+
});
|
|
1117
|
+
deps.updateHostState(state, "codex", {
|
|
1118
|
+
installed: true,
|
|
1119
|
+
version: release.tag,
|
|
1120
|
+
source: "cli-managed",
|
|
1121
|
+
skills_dir: codexResult.skills_dir,
|
|
1122
|
+
last_action: input2.action
|
|
1123
|
+
});
|
|
1124
|
+
results.push({
|
|
1125
|
+
host,
|
|
1126
|
+
ok: true,
|
|
1127
|
+
message: `Codex ${input2.action} complete (${release.tag}).`
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
const message = deps.asErrorMessage(error);
|
|
1132
|
+
failures.push({ host, error: message });
|
|
1133
|
+
results.push({
|
|
1134
|
+
host,
|
|
1135
|
+
ok: false,
|
|
1136
|
+
message
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
deps.saveState(state);
|
|
1141
|
+
if (failures.length > 0) {
|
|
1142
|
+
summarizeFailures(failures, input2.action);
|
|
1143
|
+
}
|
|
1144
|
+
return {
|
|
1145
|
+
action: input2.action,
|
|
1146
|
+
tag: release.tag,
|
|
1147
|
+
results
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
async function performUninstall2(input2) {
|
|
1151
|
+
let purge = input2.purge;
|
|
1152
|
+
if (!input2.yes) {
|
|
1153
|
+
if (!deps.isInteractive()) {
|
|
1154
|
+
throw new CliError("Uninstall requires confirmation in interactive mode. Use --yes for non-interactive execution.", {
|
|
1155
|
+
code: "uninstall_confirmation_required"
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
const plan = await deps.promptUninstallPlan(input2.hosts);
|
|
1159
|
+
if (!plan.proceed) {
|
|
1160
|
+
return {
|
|
1161
|
+
action: "uninstall",
|
|
1162
|
+
cancelled: true,
|
|
1163
|
+
purged_paths: [],
|
|
1164
|
+
results: []
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
purge = plan.purge;
|
|
1168
|
+
}
|
|
1169
|
+
const state = deps.loadState();
|
|
1170
|
+
const failures = [];
|
|
1171
|
+
const results = [];
|
|
1172
|
+
for (const host of input2.hosts) {
|
|
1173
|
+
try {
|
|
1174
|
+
if (host === "claude") {
|
|
1175
|
+
await deps.uninstallClaude();
|
|
1176
|
+
deps.updateHostState(state, "claude", {
|
|
1177
|
+
installed: false,
|
|
1178
|
+
version: null,
|
|
1179
|
+
source: null,
|
|
1180
|
+
last_action: "uninstall"
|
|
1181
|
+
});
|
|
1182
|
+
results.push({
|
|
1183
|
+
host,
|
|
1184
|
+
ok: true,
|
|
1185
|
+
message: "Claude uninstall complete."
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
if (host === "codex") {
|
|
1189
|
+
deps.uninstallCodex(true);
|
|
1190
|
+
deps.updateHostState(state, "codex", {
|
|
1191
|
+
installed: false,
|
|
1192
|
+
version: null,
|
|
1193
|
+
source: null,
|
|
1194
|
+
skills_dir: null,
|
|
1195
|
+
last_action: "uninstall"
|
|
1196
|
+
});
|
|
1197
|
+
results.push({
|
|
1198
|
+
host,
|
|
1199
|
+
ok: true,
|
|
1200
|
+
message: "Codex uninstall complete."
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
const message = deps.asErrorMessage(error);
|
|
1205
|
+
failures.push({ host, error: message });
|
|
1206
|
+
results.push({ host, ok: false, message });
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
const purged = [];
|
|
1210
|
+
if (purge) {
|
|
1211
|
+
const targets = deps.purgeTargets(input2.hosts);
|
|
1212
|
+
for (const target of targets) {
|
|
1213
|
+
deps.removePath(target);
|
|
1214
|
+
purged.push(target);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
deps.saveState(state);
|
|
1218
|
+
if (failures.length > 0) {
|
|
1219
|
+
summarizeFailures(failures, "uninstall");
|
|
1220
|
+
}
|
|
1221
|
+
return {
|
|
1222
|
+
action: "uninstall",
|
|
1223
|
+
cancelled: false,
|
|
1224
|
+
purged_paths: purged,
|
|
1225
|
+
results
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
async function readStatus2(hosts) {
|
|
1229
|
+
const state = deps.loadState();
|
|
1230
|
+
const outputHosts = {};
|
|
1231
|
+
for (const host of hosts) {
|
|
1232
|
+
if (host === "claude") {
|
|
1233
|
+
const runtime = await deps.statusClaude();
|
|
1234
|
+
const stateHost = state.hosts.claude ? {
|
|
1235
|
+
host: "claude",
|
|
1236
|
+
installed: state.hosts.claude.installed,
|
|
1237
|
+
version: state.hosts.claude.version ?? null,
|
|
1238
|
+
source: state.hosts.claude.source ?? null,
|
|
1239
|
+
last_action: state.hosts.claude.last_action
|
|
1240
|
+
} : void 0;
|
|
1241
|
+
outputHosts.claude = mergeStatus(stateHost, runtime);
|
|
1242
|
+
}
|
|
1243
|
+
if (host === "codex") {
|
|
1244
|
+
const runtime = deps.statusCodex(true);
|
|
1245
|
+
const stateHost = state.hosts.codex ? {
|
|
1246
|
+
host: "codex",
|
|
1247
|
+
installed: state.hosts.codex.installed,
|
|
1248
|
+
version: state.hosts.codex.version ?? null,
|
|
1249
|
+
source: state.hosts.codex.source ?? null,
|
|
1250
|
+
last_action: state.hosts.codex.last_action
|
|
1251
|
+
} : void 0;
|
|
1252
|
+
outputHosts.codex = mergeStatus(stateHost, runtime);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return {
|
|
1256
|
+
schema_version: state.schema_version,
|
|
1257
|
+
state_file: deps.lineupStateFile(),
|
|
1258
|
+
hosts: outputHosts
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
return {
|
|
1262
|
+
performInstallOrUpdate: performInstallOrUpdate2,
|
|
1263
|
+
performUninstall: performUninstall2,
|
|
1264
|
+
readStatus: readStatus2
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
var operations = createOperations();
|
|
1268
|
+
var performInstallOrUpdate = operations.performInstallOrUpdate;
|
|
1269
|
+
var performUninstall = operations.performUninstall;
|
|
1270
|
+
var readStatus = operations.readStatus;
|
|
1271
|
+
|
|
1272
|
+
// src/lib/hosts.ts
|
|
1273
|
+
var HOST_SET = /* @__PURE__ */ new Set([...SUPPORTED_HOSTS, "all"]);
|
|
1274
|
+
function normalizeHostOption(raw) {
|
|
1275
|
+
if (!raw) {
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
const normalized = raw.trim().toLowerCase();
|
|
1279
|
+
if (!HOST_SET.has(normalized)) {
|
|
1280
|
+
throw new CliError(`Invalid --host value: ${raw}. Expected claude, codex, or all.`, {
|
|
1281
|
+
code: "invalid_host"
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
return normalized;
|
|
1285
|
+
}
|
|
1286
|
+
function hostOptionToHosts(option) {
|
|
1287
|
+
if (option === "all") {
|
|
1288
|
+
return [...SUPPORTED_HOSTS];
|
|
1289
|
+
}
|
|
1290
|
+
return [option];
|
|
1291
|
+
}
|
|
1292
|
+
async function resolveRequestedHosts(rawHost, options) {
|
|
1293
|
+
const normalized = normalizeHostOption(rawHost);
|
|
1294
|
+
if (normalized) {
|
|
1295
|
+
return hostOptionToHosts(normalized);
|
|
1296
|
+
}
|
|
1297
|
+
const interactive = options?.interactive ?? isInteractive();
|
|
1298
|
+
if (interactive) {
|
|
1299
|
+
return (options?.prompt ?? promptHostSelection)();
|
|
1300
|
+
}
|
|
1301
|
+
throw new CliError("No host selected. Use --host claude|codex|all when running non-interactively.", {
|
|
1302
|
+
code: "host_required"
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// src/commands/install.ts
|
|
1307
|
+
async function runInstallCommand(options) {
|
|
1308
|
+
const hosts = await resolveRequestedHosts(options.host);
|
|
1309
|
+
const result = await performInstallOrUpdate({
|
|
1310
|
+
action: "install",
|
|
1311
|
+
hosts,
|
|
1312
|
+
version: options.version,
|
|
1313
|
+
fromDir: options.fromDir,
|
|
1314
|
+
yes: Boolean(options.yes)
|
|
1315
|
+
});
|
|
1316
|
+
printTableLine(`Installed Lineup ${result.tag} for: ${hosts.join(", ")}`);
|
|
1317
|
+
for (const item of result.results) {
|
|
1318
|
+
printTableLine(`- ${item.host}: ${item.ok ? "ok" : "failed"} (${item.message})`);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/commands/status.ts
|
|
1323
|
+
function printHostStatus(host, item) {
|
|
1324
|
+
printTableLine(`- ${host}: ${item.installed ? "installed" : "not installed"}`);
|
|
1325
|
+
printTableLine(` version: ${item.version ?? "unknown"}`);
|
|
1326
|
+
printTableLine(` source: ${item.source ?? "unknown"}`);
|
|
1327
|
+
printTableLine(` last_action: ${item.last_action ?? "none"}`);
|
|
1328
|
+
if (item.error) {
|
|
1329
|
+
printTableLine(` error: ${item.error}`);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
async function runStatusCommand(options) {
|
|
1333
|
+
const hosts = await resolveRequestedHosts(options.host);
|
|
1334
|
+
const status = await readStatus(hosts);
|
|
1335
|
+
if (options.json) {
|
|
1336
|
+
printJson(status);
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
for (const host of hosts) {
|
|
1340
|
+
const item = status.hosts[host];
|
|
1341
|
+
if (!item) {
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
printHostStatus(host, item);
|
|
1345
|
+
}
|
|
1346
|
+
printTableLine(`state_file: ${status.state_file}`);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/commands/uninstall.ts
|
|
1350
|
+
async function runUninstallCommand(options) {
|
|
1351
|
+
const hosts = await resolveRequestedHosts(options.host);
|
|
1352
|
+
const result = await performUninstall({
|
|
1353
|
+
hosts,
|
|
1354
|
+
yes: Boolean(options.yes),
|
|
1355
|
+
purge: Boolean(options.purge)
|
|
1356
|
+
});
|
|
1357
|
+
if (result.cancelled) {
|
|
1358
|
+
printTableLine("Uninstall cancelled.");
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
printTableLine(`Uninstalled Lineup for: ${hosts.join(", ")}`);
|
|
1362
|
+
for (const item of result.results) {
|
|
1363
|
+
printTableLine(`- ${item.host}: ${item.ok ? "ok" : "failed"} (${item.message})`);
|
|
1364
|
+
}
|
|
1365
|
+
if (result.purged_paths.length > 0) {
|
|
1366
|
+
printTableLine("Purged data paths:");
|
|
1367
|
+
for (const target of result.purged_paths) {
|
|
1368
|
+
printTableLine(`- ${target}`);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// src/commands/update.ts
|
|
1374
|
+
async function runUpdateCommand(options) {
|
|
1375
|
+
const hosts = await resolveRequestedHosts(options.host);
|
|
1376
|
+
const result = await performInstallOrUpdate({
|
|
1377
|
+
action: "update",
|
|
1378
|
+
hosts,
|
|
1379
|
+
version: options.version,
|
|
1380
|
+
fromDir: options.fromDir,
|
|
1381
|
+
yes: Boolean(options.yes)
|
|
1382
|
+
});
|
|
1383
|
+
printTableLine(`Updated Lineup ${result.tag} for: ${hosts.join(", ")}`);
|
|
1384
|
+
for (const item of result.results) {
|
|
1385
|
+
printTableLine(`- ${item.host}: ${item.ok ? "ok" : "failed"} (${item.message})`);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// src/cli.ts
|
|
1390
|
+
function packageVersion() {
|
|
1391
|
+
const packageJsonPath = path8.join(packageRoot(), "package.json");
|
|
1392
|
+
const raw = readFileSync6(packageJsonPath, "utf8");
|
|
1393
|
+
const parsed = JSON.parse(raw);
|
|
1394
|
+
return parsed.version ?? "0.0.0";
|
|
1395
|
+
}
|
|
1396
|
+
function buildProgram(handlers) {
|
|
1397
|
+
const commandHandlers = {
|
|
1398
|
+
install: runInstallCommand,
|
|
1399
|
+
update: runUpdateCommand,
|
|
1400
|
+
uninstall: runUninstallCommand,
|
|
1401
|
+
status: runStatusCommand,
|
|
1402
|
+
...handlers
|
|
1403
|
+
};
|
|
1404
|
+
const program = new Command();
|
|
1405
|
+
program.name("lineup").description("Lineup multi-host manager for Claude Code and Codex").version(packageVersion(), "--cli-version", "output CLI version").showHelpAfterError();
|
|
1406
|
+
program.command("install").description("Install Lineup for selected host(s)").option("--host <host>", "Target host(s): claude|codex|all").option("--version <tag>", "Release tag to install", "latest").option("--from-dir <path>", "Install from local directory instead of GitHub release").option("--yes", "Auto-confirm prompts").action(commandHandlers.install);
|
|
1407
|
+
program.command("update").description("Update Lineup for selected host(s)").option("--host <host>", "Target host(s): claude|codex|all").option("--version <tag>", "Release tag to install", "latest").option("--from-dir <path>", "Install from local directory instead of GitHub release").option("--yes", "Auto-confirm prompts").action(commandHandlers.update);
|
|
1408
|
+
program.command("uninstall").description("Uninstall Lineup for selected host(s)").option("--host <host>", "Target host(s): claude|codex|all").option("--yes", "Auto-confirm prompts").option("--purge", "Purge Lineup data directories").action(commandHandlers.uninstall);
|
|
1409
|
+
program.command("status").description("Show Lineup installation status").option("--host <host>", "Target host(s): claude|codex|all").option("--json", "Emit machine-readable JSON output").action(commandHandlers.status);
|
|
1410
|
+
return program;
|
|
1411
|
+
}
|
|
1412
|
+
function printCliError(error) {
|
|
1413
|
+
const message = asErrorMessage(error);
|
|
1414
|
+
process2.stderr.write(`${message}
|
|
1415
|
+
`);
|
|
1416
|
+
}
|
|
1417
|
+
function resolveExitCode(error) {
|
|
1418
|
+
if (error instanceof CliError) {
|
|
1419
|
+
return error.exitCode;
|
|
1420
|
+
}
|
|
1421
|
+
return 1;
|
|
1422
|
+
}
|
|
1423
|
+
function handleFatalError(error) {
|
|
1424
|
+
printCliError(error);
|
|
1425
|
+
process2.exit(resolveExitCode(error));
|
|
1426
|
+
}
|
|
1427
|
+
async function run(argv = process2.argv) {
|
|
1428
|
+
const program = buildProgram();
|
|
1429
|
+
await program.parseAsync(argv);
|
|
1430
|
+
}
|
|
1431
|
+
function isDirectExecution(argv) {
|
|
1432
|
+
const entry = argv[1];
|
|
1433
|
+
if (!entry) {
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
1436
|
+
return path8.resolve(entry) === path8.resolve(packageRoot(), "dist", "cli.js");
|
|
1437
|
+
}
|
|
1438
|
+
if (isDirectExecution(process2.argv)) {
|
|
1439
|
+
run().catch(handleFatalError);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
export {
|
|
1443
|
+
validateHostAdapter,
|
|
1444
|
+
validateInstallerState,
|
|
1445
|
+
validateReleaseManifest,
|
|
1446
|
+
buildProgram,
|
|
1447
|
+
printCliError,
|
|
1448
|
+
resolveExitCode,
|
|
1449
|
+
handleFatalError,
|
|
1450
|
+
run
|
|
1451
|
+
};
|