diff --git a/go/kitup.go b/go/kitup.go index ac12bb9..7e94137 100644 --- a/go/kitup.go +++ b/go/kitup.go @@ -437,15 +437,9 @@ type bundleMetadata struct { Explicit bool } -type bundleFile struct { - Path string - Contents []byte - Mode fs.FileMode -} - type normalizedSkillBundle struct { - Files []bundleFile - ByPath map[string]bundleFile + Files []SkillFile + ByPath map[string]SkillFile } var skillNamePattern = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`) @@ -812,10 +806,6 @@ func RunBundledSkillInstall(opts InstallWorkflowOptions) (InstallWorkflowReport, if info, err := file.Stat(); err == nil && info.Mode()&os.ModeCharDevice != 0 { opts.StdinTTY = true } - } else if in == os.Stdin { - if info, err := os.Stdin.Stat(); err == nil && info.Mode()&os.ModeCharDevice != 0 { - opts.StdinTTY = true - } } } out := opts.Out @@ -884,9 +874,6 @@ func RunBundledSkillInstall(opts InstallWorkflowOptions) (InstallWorkflowReport, return InstallWorkflowReport{Selection: selection, Scope: scope, Plan: plan, Report: report, DryRun: opts.DryRun}, nil } renderInstallSummary(out, plan) - if len(plan.Installed)+len(plan.Updated) == 0 { - return InstallWorkflowReport{Selection: selection, Scope: scope, Plan: plan, Report: plan}, nil - } if selection.NeedsConfirmation { confirmed, err := promptConfirmation(reader, out) if err != nil { @@ -1861,7 +1848,7 @@ func readFSBundleFiles(fsys fs.FS, root string) ([]SkillFile, error) { } func normalizeSkillFiles(files []SkillFile) (normalizedSkillBundle, error) { - byPath := map[string]bundleFile{} + byPath := map[string]SkillFile{} for _, file := range files { normalizedPath, include, err := normalizeBundlePath(file.Path) if err != nil { @@ -1877,14 +1864,14 @@ func normalizeSkillFiles(files []SkillFile) (normalizedSkillBundle, error) { if mode == 0 { mode = defaultBundleFileMode(normalizedPath) } - byPath[normalizedPath] = bundleFile{Path: normalizedPath, Contents: file.Contents, Mode: mode} + byPath[normalizedPath] = SkillFile{Path: normalizedPath, Contents: file.Contents, Mode: mode} } paths := make([]string, 0, len(byPath)) for path := range byPath { paths = append(paths, path) } sort.Strings(paths) - normalized := normalizedSkillBundle{Files: make([]bundleFile, 0, len(paths)), ByPath: byPath} + normalized := normalizedSkillBundle{Files: make([]SkillFile, 0, len(paths)), ByPath: byPath} for _, path := range paths { normalized.Files = append(normalized.Files, byPath[path]) } @@ -1917,14 +1904,6 @@ func normalizeBundlePath(value string) (string, bool, error) { return strings.Join(parts, "/"), true, nil } -func copySkillBundleDir(src, dest string) error { - bundle, err := readSkillBundle(DirectoryBundle(src)) - if err != nil { - return err - } - return copySkillBundle(bundle, dest) -} - func skipName(name string) bool { return name == ".git" || name == ".kitup.json" || name == ".DS_Store" || strings.HasSuffix(name, ".swp") || strings.HasSuffix(name, "~") } diff --git a/python/src/kitup/types.py b/python/src/kitup/types.py index a10a6dc..35b33ce 100644 --- a/python/src/kitup/types.py +++ b/python/src/kitup/types.py @@ -90,7 +90,6 @@ class BundleFile: class NormalizedSkillBundle: files: list[BundleFile] by_path: dict[str, BundleFile] - label: str | None = None @dataclass(frozen=True) diff --git a/python/src/kitup/workflow.py b/python/src/kitup/workflow.py index e678c57..b867169 100644 --- a/python/src/kitup/workflow.py +++ b/python/src/kitup/workflow.py @@ -21,22 +21,7 @@ def split_flag_values(values: list[str]) -> list[str]: - return [ - part.strip() - for value in values - for part in value.replace(",", " ").split() - if part.strip() - ] - - -def dedupe(values: list[str]) -> list[str]: - seen: set[str] = set() - result: list[str] = [] - for value in values: - if value not in seen: - seen.add(value) - result.append(value) - return result + return [part for value in values for part in value.replace(",", " ").split()] def parse_scope_flag( @@ -68,7 +53,7 @@ def agent_selector_from_flags( } ) return "*" - return dedupe(agents) + return list(dict.fromkeys(agents)) def parse_install_flags(flags: dict[str, object]) -> ParsedInstallFlags: @@ -201,7 +186,7 @@ def run_bundled_skill_install_with_io( output: object | None, ) -> InstallWorkflowReport: reader = _LineReader(input) - writer = _coerce_output(output) + writer = _OutputWriter(output) scope, scope_error = _resolve_workflow_scope( reader=reader, output=writer, @@ -217,8 +202,8 @@ def run_bundled_skill_install_with_io( return InstallWorkflowReport( selection=scope_error, scope=scope, - plan=empty_install_report(), - report=empty_install_report(), + plan=InstallReport(), + report=InstallReport(), canceled=False, dry_run=options.dry_run, ) @@ -238,8 +223,8 @@ def run_bundled_skill_install_with_io( return InstallWorkflowReport( selection=selection, scope=scope, - plan=empty_install_report(), - report=empty_install_report(), + plan=InstallReport(), + report=InstallReport(), canceled=False, dry_run=options.dry_run, ) @@ -255,8 +240,8 @@ def run_bundled_skill_install_with_io( return InstallWorkflowReport( selection=selection, scope=scope, - plan=empty_install_report(), - report=empty_install_report(), + plan=InstallReport(), + report=InstallReport(), canceled=True, dry_run=options.dry_run, ) @@ -297,21 +282,12 @@ def run_bundled_skill_install_with_io( dry_run=False, ) _render_install_summary(writer, plan) - if not _has_install_writes(plan): - return InstallWorkflowReport( - selection=selection, - scope=scope, - plan=plan, - report=plan, - canceled=False, - dry_run=False, - ) if selection.needs_confirmation and not _prompt_confirmation(reader, writer): return InstallWorkflowReport( selection=selection, scope=scope, plan=plan, - report=empty_install_report(), + report=InstallReport(), canceled=True, dry_run=False, ) @@ -327,10 +303,6 @@ def run_bundled_skill_install_with_io( ) -def empty_install_report(errors: list[object] | None = None) -> InstallReport: - return InstallReport(errors=list(errors or [])) - - def _resolve_workflow_scope( *, reader: "_LineReader", @@ -478,10 +450,6 @@ def _has_visible_install_plan(report: InstallReport) -> bool: ) -def _has_install_writes(report: InstallReport) -> bool: - return len(report.installed) + len(report.updated) > 0 - - def _install_selection( selected_host_ids: list[str], detected_host_ids: list[str], @@ -548,11 +516,7 @@ def _coerce_optional_text(value: object) -> str | None: def _coerce_flag_values(value: object) -> list[str]: - if value is None: - return [] - if isinstance(value, list): - return [item for item in value if isinstance(item, str)] - if isinstance(value, tuple): + if isinstance(value, (list, tuple)): return [item for item in value if isinstance(item, str)] if isinstance(value, str): return [value] @@ -592,14 +556,14 @@ def _iter_lines(self, source: object | None) -> Iterable[str]: if source is None: return [] if isinstance(source, bytes): - return self._split_text(source.decode("utf-8")) + return source.decode("utf-8").splitlines() if isinstance(source, str): - return self._split_text(source) + return source.splitlines() if hasattr(source, "read"): contents = source.read() if isinstance(contents, bytes): - return self._split_text(contents.decode("utf-8")) - return self._split_text(str(contents)) + return contents.decode("utf-8").splitlines() + return str(contents).splitlines() if isinstance(source, Iterable): chunks: list[str] = [] for item in source: @@ -607,20 +571,9 @@ def _iter_lines(self, source: object | None) -> Iterable[str]: chunks.append(item.decode("utf-8")) else: chunks.append(str(item)) - return self._split_text("".join(chunks)) + return "".join(chunks).splitlines() return [] - @staticmethod - def _split_text(text: str) -> list[str]: - if text == "": - return [] - lines = text.splitlines() - if text.endswith(("\n", "\r")): - return [line.rstrip("\r") for line in lines] - if not lines: - return [text.rstrip("\r")] - return [line.rstrip("\r") for line in lines] - class _OutputWriter: def __init__(self, target: object | None) -> None: @@ -630,7 +583,3 @@ def write(self, chunk: str) -> None: if self._target is None: return self._target.write(chunk) - - -def _coerce_output(output: object | None) -> _OutputWriter: - return _OutputWriter(output) diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 5176a1d..fb095b0 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -395,7 +395,6 @@ struct BundleFile { #[derive(Clone, Debug)] struct NormalizedSkillBundle { - files: Vec, by_path: BTreeMap, } @@ -907,7 +906,7 @@ pub fn compute_bundle_content_hash(bundle: &SkillBundle) -> io::Result { fn content_hash(bundle: &NormalizedSkillBundle) -> String { let mut hash = Sha256::new(); - for file in &bundle.files { + for file in bundle.by_path.values() { hash.update(file.path.as_bytes()); hash.update([0]); hash.update(&file.contents); @@ -1034,16 +1033,6 @@ pub fn run_bundled_skill_install_with_io( }); } render_install_summary(output, &plan)?; - if plan.installed.len() + plan.updated.len() == 0 { - return Ok(InstallWorkflowReport { - selection, - scope: workflow_scope, - plan: plan.clone(), - report: plan, - canceled: false, - dry_run: false, - }); - } if selection.needs_confirmation && !prompt_confirmation(input, output)? { return Ok(InstallWorkflowReport { selection, @@ -1416,7 +1405,7 @@ fn remove_managed_skill( fn copy_skill_bundle(bundle: &NormalizedSkillBundle, dest: &Path) -> io::Result<()> { fs::create_dir_all(dest)?; - for file in &bundle.files { + for file in bundle.by_path.values() { let to = dest.join(PathBuf::from( file.path.replace('/', std::path::MAIN_SEPARATOR_STR), )); @@ -1435,7 +1424,7 @@ fn repair_skill_bundle_modes( write: bool, ) -> io::Result { let mut repaired = false; - for file in &bundle.files { + for file in bundle.by_path.values() { let to = dest.join(PathBuf::from( file.path.replace('/', std::path::MAIN_SEPARATOR_STR), )); @@ -2169,8 +2158,7 @@ fn normalize_skill_files(files: Vec) -> io::Result u32 { @@ -2192,9 +2180,8 @@ fn normalize_bundle_path(value: &str) -> io::Result> { format!("invalid skill file path: {value}"), )); } - let parts: Vec<_> = value.split('/').collect(); - for part in &parts { - if part.is_empty() || *part == "." || *part == ".." { + for part in value.split('/') { + if part.is_empty() || part == "." || part == ".." { return Err(io::Error::new( io::ErrorKind::InvalidInput, format!("invalid skill file path: {value}"), @@ -2204,7 +2191,7 @@ fn normalize_bundle_path(value: &str) -> io::Result> { return Ok(None); } } - Ok(Some(parts.join("/"))) + Ok(Some(value.to_string())) } #[cfg(unix)] diff --git a/tests/go-golden/golden_test.go b/tests/go-golden/golden_test.go index 61a648b..d6208f0 100644 --- a/tests/go-golden/golden_test.go +++ b/tests/go-golden/golden_test.go @@ -231,7 +231,9 @@ func setupGiven(t *testing.T, tc goldenCase, home, workspace string) { } if target, ok := tc.Given["copySkillBundleTo"].(string); ok { must(t, os.RemoveAll(expandString(target, home, workspace))) - must(t, copySkillBundleDir(caseSkillBundleDir(tc), expandString(target, home, workspace))) + bundle, err := readSkillBundle(DirectoryBundle(caseSkillBundleDir(tc))) + must(t, err) + must(t, copySkillBundle(bundle, expandString(target, home, workspace))) } if modes, ok := tc.Given["fileModes"].(map[string]any); ok { for path, mode := range modes { diff --git a/ts/src/index.ts b/ts/src/index.ts index 99c1ffa..bd647fb 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -294,7 +294,6 @@ interface BundleFile { } interface NormalizedSkillBundle { - label?: string; files: BundleFile[]; byPath: Map; } @@ -754,17 +753,6 @@ export async function runBundledSkillInstall( } renderInstallSummary(output, plan); - if (plan.installed.length + plan.updated.length === 0) { - return { - selection, - scope, - plan, - report: plan, - canceled: false, - dryRun: false, - }; - } - if (selection.needsConfirmation) { const confirmed = await promptConfirmation(reader, output); if (!confirmed) { @@ -1006,9 +994,9 @@ async function resolveSkillBundle( }; } if (bundle.kind === "directory") { - const dir = resolvePath(bundle.path, cwd); + const dir = resolve(cwd, bundle.path); return { - bundle: normalizeSkillFiles(await readDirectoryBundleFiles(dir), dir), + bundle: normalizeSkillFiles(await readDirectoryBundleFiles(dir)), metadata: { source: "bundled" }, }; } @@ -1157,10 +1145,7 @@ async function readDirectoryBundleFiles( return files; } -function normalizeSkillFiles( - files: SkillFile[], - label?: string, -): NormalizedSkillBundle { +function normalizeSkillFiles(files: SkillFile[]): NormalizedSkillBundle { const byPath = new Map(); for (const file of files) { const normalizedPath = normalizeBundlePath(file.path); @@ -1180,7 +1165,7 @@ function normalizeSkillFiles( const normalizedFiles = [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path), ); - return { label, files: normalizedFiles, byPath }; + return { files: normalizedFiles, byPath }; } function bundleFileMode(path: string, mode: number | undefined) { @@ -1955,10 +1940,6 @@ function expandHostPath(path: string, home: string, cwd: string) { return path.startsWith("~/") ? join(home, path.slice(2)) : join(cwd, path); } -function resolvePath(path: string, cwd: string) { - return resolve(cwd, path); -} - function parseFrontmatter(content: string) { const values = new Map(); for (const line of content.split(/\r?\n/)) { @@ -1968,17 +1949,6 @@ function parseFrontmatter(content: string) { return values; } -async function listSkillFiles(dir: string, base = dir): Promise { - const files: string[] = []; - for (const entry of await readdir(dir, { withFileTypes: true })) { - if (skipName(entry.name)) continue; - const full = join(dir, entry.name); - if (entry.isDirectory()) files.push(...(await listSkillFiles(full, base))); - if (entry.isFile()) files.push(relative(base, full).split(sep).join("/")); - } - return files.sort(); -} - function skipName(name: string) { return ( name === ".git" ||