mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 00:49:41 +02:00
refactor(install): drive CLI detection, the install menu and hints from the catalogue
install.sh carried nine search-path arrays, eighteen near-identical
check_<cli>/get_<cli>_path functions, and three separately hand-maintained
enumerations of all nine CLIs. They had to agree and did not: upstream b6d0f1fa
is "wire OMP into install.sh's CLI detection (it had none)", and the section
comment above the roll-call named six of the nine.
All of it now reads the generated catalogue. `detect_all_clis` resolves every
CLI in one memoized pass into CLI_FOUND_PATH/CLI_FOUND_COUNT; `check_cli` and
`get_cli_path` replace the eighteen pairs; the roll-call, the "no AI CLI found"
gate and the closing reminder become loops. Probe order per CLI is unchanged and
`test/install-sh-detection-parity.test.ts` proves it against the literals
transcribed from the arrays this deletes.
Behaviour changes worth naming:
- The install menu is built from the catalogue, so it offers every enabled CLI
that is not installed and ships a command — five instead of two. Gemini had a
command in the registry and appeared in NO list in this script.
- Its labels are now the registry's ("Claude" rather than "Claude Code"), the
same trade PR A made for `codeman doctor` rows. A suffix map would just be the
hand-maintained list again.
- On a wget-only host the menu prints commands instead of running them. The
registry's commands call curl, whereas the two literals this replaces went
through download_to_stdout; rewriting curl to wget inside a string we are
about to execute is the wrong instinct.
The trust boundary is mechanical, not a promise: CLI_INSTALL_CMD_TRUSTED is
written only from the generated per-platform arrays and is the only thing ever
executed; CLI_INSTALL_CMD_DISPLAY is what the optional, opt-in refresh may
rewrite. The refresh warns on all three failure shapes — empty body, unparseable
content, failed fetch — which is the silent-degradation bug from the review, and
it parses with node into tab-separated records read by `read`, never eval.
Bash 3.2 throughout (macOS ships it): parallel indexed arrays, offset/length
windows instead of delimiters, no associative arrays, namerefs, mapfile or
here-strings. Verified by executing the script under a real bash 3.2 container,
which is also now a CI step alongside `bash -n` and a catalogue `--check` — the
empty-window case (`shell` has no binaries) is a runtime `set -u` abort that
`bash -n` cannot see. Running it that way caught `detect_os` being called inside
the platform loop: ten forks, and ten copies of one error, since a `die` inside
`$( )` can only exit the subshell.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EMxQreQUZX5ZyybxAGh12
This commit is contained in:
@@ -101,14 +101,32 @@ const ARRAY_PREFIX_TO_CLI_ID: Record<string, string> = {
|
||||
OMP: 'omp',
|
||||
};
|
||||
|
||||
/** Every `NAME_SEARCH_PATHS=( "a" "b" )` block in install.sh, in declaration order. */
|
||||
/**
|
||||
* The per-CLI search paths install.sh will actually probe, read back out of the GENERATED
|
||||
* block: `CLI_ALL_PATHS` sliced by each id's `CLI_PATH_OFF`/`CLI_PATH_LEN` window.
|
||||
*
|
||||
* This parser replaced one that read the nine hand-written `*_SEARCH_PATHS` arrays, which
|
||||
* this change deletes. The literals below did NOT move: they are still the same strings
|
||||
* transcribed from those arrays, so the pin still measures the generated block against what
|
||||
* shipped before it existed, which is the only comparison worth making.
|
||||
*/
|
||||
function parseInstallShSearchPaths(source: string): Record<string, string[]> {
|
||||
const readArray = (name: string): string[] => {
|
||||
const m = new RegExp(`^${name}=\\((.*)\\)$`, 'm').exec(source);
|
||||
if (!m) throw new Error(`install.sh has no ${name}= array`);
|
||||
// Tokens are double-quoted (paths, which carry $HOME), single-quoted (ids, labels) or
|
||||
// bare (the numeric offset/length windows).
|
||||
return [...m[1].matchAll(/"([^"]*)"|'([^']*)'|(\S+)/g)].map((t) => t[1] ?? t[2] ?? t[3]);
|
||||
};
|
||||
const ids = readArray('CLI_IDS');
|
||||
const paths = readArray('CLI_ALL_PATHS');
|
||||
const offs = readArray('CLI_PATH_OFF').map(Number);
|
||||
const lens = readArray('CLI_PATH_LEN').map(Number);
|
||||
const out: Record<string, string[]> = {};
|
||||
const block = /^([A-Z0-9_]+)_SEARCH_PATHS=\(\s*\n([\s\S]*?)^\)/gm;
|
||||
for (const match of source.matchAll(block)) {
|
||||
const entries = [...match[2].matchAll(/^\s*"([^"]+)"\s*$/gm)].map((m) => m[1]);
|
||||
out[match[1]] = entries;
|
||||
}
|
||||
ids.forEach((id, i) => {
|
||||
const prefix = Object.entries(ARRAY_PREFIX_TO_CLI_ID).find(([, cliId]) => cliId === id)?.[0];
|
||||
if (prefix) out[prefix] = paths.slice(offs[i], offs[i] + lens[i]);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -131,20 +149,20 @@ function registrySearchPaths(cliId: string): string[] {
|
||||
describe('install.sh CLI detection parity', () => {
|
||||
const parsed = parseInstallShSearchPaths(INSTALL_SH);
|
||||
|
||||
it('finds every declared search-path array (anti-vacuity)', () => {
|
||||
it('finds every generated search-path window (anti-vacuity)', () => {
|
||||
// If the parse returns nothing, every it.each below passes by comparing [] to [].
|
||||
expect(Object.keys(parsed).sort()).toEqual(Object.keys(LITERAL_SEARCH_PATHS).sort());
|
||||
for (const [name, paths] of Object.entries(parsed)) {
|
||||
expect(paths.length, `${name}_SEARCH_PATHS parsed empty`).toBeGreaterThan(0);
|
||||
expect(paths.length, `${name} window parsed empty`).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(Object.keys(LITERAL_SEARCH_PATHS))('%s_SEARCH_PATHS matches the pinned literals', (prefix) => {
|
||||
it.each(Object.keys(LITERAL_SEARCH_PATHS))('%s search paths match the pinned literals', (prefix) => {
|
||||
expect(parsed[prefix]).toEqual(LITERAL_SEARCH_PATHS[prefix]);
|
||||
});
|
||||
|
||||
it.each(Object.entries(ARRAY_PREFIX_TO_CLI_ID))(
|
||||
'%s_SEARCH_PATHS is reproduced by registry entry "%s"',
|
||||
'%s search paths are reproduced by registry entry "%s"',
|
||||
(prefix, cliId) => {
|
||||
// The claim the generator rests on: the registry already knows every path the
|
||||
// installer probes, in the same order. A failure here means the generated block would
|
||||
|
||||
Reference in New Issue
Block a user