coreslopper 0.0.0 → 0.2.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/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // packages/cli/src/npx.ts
4
4
  import { Command } from "commander";
5
- import { relative as relative3, resolve as resolve7 } from "path";
5
+ import { relative as relative4, resolve as resolve8 } from "path";
6
6
 
7
7
  // packages/cli/src/doctor.ts
8
8
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
@@ -1464,14 +1464,14 @@ async function runDoctor(root, options = {}) {
1464
1464
  }
1465
1465
 
1466
1466
  // packages/cli/src/init-command.ts
1467
- import { resolve as resolve5 } from "path";
1467
+ import { resolve as resolve6 } from "path";
1468
1468
 
1469
1469
  // packages/cli/src/init.ts
1470
- import { spawnSync as spawnSync2 } from "child_process";
1471
- import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync } from "fs";
1472
- import { chmod, cp as cp2, mkdir as mkdir6, readFile as readFile7, readdir, stat as stat2, writeFile as writeFile6 } from "fs/promises";
1470
+ import { spawnSync as spawnSync3 } from "child_process";
1471
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
1472
+ import { chmod as chmod2, cp as cp2, mkdir as mkdir7, readFile as readFile8, readdir as readdir2, stat as stat3, writeFile as writeFile6 } from "fs/promises";
1473
1473
  import { homedir as homedir2 } from "os";
1474
- import { basename, isAbsolute, join as join9, parse, relative as relative2, resolve as resolve3, sep } from "path";
1474
+ import { basename, isAbsolute, join as join10, parse, relative as relative3, resolve as resolve4, sep as sep2 } from "path";
1475
1475
 
1476
1476
  // packages/cli/src/new-project.ts
1477
1477
  import { cp, mkdir as mkdir5, readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
@@ -1585,7 +1585,12 @@ function relativeToRoot(path, root) {
1585
1585
  return path.startsWith(root) ? path.slice(root.length + 1) : path;
1586
1586
  }
1587
1587
 
1588
- // packages/cli/src/init.ts
1588
+ // packages/cli/src/update.ts
1589
+ import { spawnSync as spawnSync2 } from "child_process";
1590
+ import { createHash as createHash4 } from "crypto";
1591
+ import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync } from "fs";
1592
+ import { chmod, copyFile as copyFile2, mkdir as mkdir6, readFile as readFile7, readdir, rm, rmdir, stat as stat2 } from "fs/promises";
1593
+ import { dirname as dirname4, join as join9, posix, relative as relative2, resolve as resolve3, sep } from "path";
1589
1594
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
1590
1595
  ".git",
1591
1596
  ".cache",
@@ -1599,13 +1604,270 @@ var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
1599
1604
  "runtime",
1600
1605
  "tmp"
1601
1606
  ]);
1607
+ var CONFLICT_SUFFIX = ".coreslopper-new";
1608
+ var USER_OWNED_FILES = /* @__PURE__ */ new Set([".mcp.json", WORKSPACE_MARKER, "GETTING_STARTED.md"]);
1609
+ var ALWAYS_UPSTREAM_FILES = /* @__PURE__ */ new Set(["package-lock.json"]);
1610
+ var USER_OWNED_PREFIXES = ["projects/"];
1611
+ function isRuntimePath(path) {
1612
+ if (!path) return true;
1613
+ const segments = path.split("/");
1614
+ if (segments.some((segment) => SKIPPED_DIRECTORIES.has(segment))) return false;
1615
+ if (segments.at(-1)?.endsWith(CONFLICT_SUFFIX)) return false;
1616
+ if (path === ".env" || path.startsWith(".env.") && path !== ".env.example") return false;
1617
+ return true;
1618
+ }
1619
+ function toPosix(path) {
1620
+ return sep === "/" ? path : path.split(sep).join(posix.sep);
1621
+ }
1622
+ async function listRuntimeFiles(runtime) {
1623
+ const files = [];
1624
+ async function walk(directory) {
1625
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
1626
+ const absolute = join9(directory, entry.name);
1627
+ const path = toPosix(relative2(runtime, absolute));
1628
+ if (!isRuntimePath(path)) continue;
1629
+ if (entry.isDirectory()) await walk(absolute);
1630
+ else if (entry.isFile()) files.push(path);
1631
+ }
1632
+ }
1633
+ await walk(runtime);
1634
+ return files.sort();
1635
+ }
1636
+ async function hashFile2(path) {
1637
+ return createHash4("sha256").update(await readFile7(path)).digest("hex");
1638
+ }
1639
+ async function hashRuntime(runtime) {
1640
+ const hashes = {};
1641
+ for (const file of await listRuntimeFiles(runtime)) hashes[file] = await hashFile2(join9(runtime, file));
1642
+ return hashes;
1643
+ }
1644
+ function readMarker(target) {
1645
+ const path = join9(target, WORKSPACE_MARKER);
1646
+ if (!existsSync6(path)) return void 0;
1647
+ try {
1648
+ return JSON.parse(readFileSync5(path, "utf8"));
1649
+ } catch {
1650
+ return void 0;
1651
+ }
1652
+ }
1653
+ function writeMarkerFile(target, marker) {
1654
+ writeFileSync(join9(target, WORKSPACE_MARKER), `${JSON.stringify(marker, null, 2)}
1655
+ `, "utf8");
1656
+ }
1657
+ function compareVersions(a, b) {
1658
+ const parse2 = (value) => value.split("-")[0].split(".").map((part) => Number(part) || 0);
1659
+ const left = parse2(a);
1660
+ const right = parse2(b);
1661
+ for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
1662
+ const difference = (left[index] ?? 0) - (right[index] ?? 0);
1663
+ if (difference !== 0) return Math.sign(difference);
1664
+ }
1665
+ return 0;
1666
+ }
1667
+ function resolveRuntime(runtimeDir2) {
1668
+ const runtime = runtimeDir2 ? resolve3(runtimeDir2) : runtimeDir();
1669
+ if (!runtime || !existsSync6(join9(runtime, "templates", "project"))) {
1670
+ throw new ValidationError('Runtime assets not found. Run "npx coreslopper@latest update" from the workspace.');
1671
+ }
1672
+ return runtime;
1673
+ }
1674
+ function runtimeVersion(runtime) {
1675
+ try {
1676
+ const packageJson = JSON.parse(readFileSync5(join9(runtime, "package.json"), "utf8"));
1677
+ return packageJson.version ?? ownVersion();
1678
+ } catch {
1679
+ return ownVersion();
1680
+ }
1681
+ }
1682
+ function gitIsDirty(target) {
1683
+ if (!existsSync6(join9(target, ".git"))) return false;
1684
+ const result = spawnSync2("git", ["status", "--porcelain"], { cwd: target, encoding: "utf8" });
1685
+ return result.status === 0 && result.stdout.trim().length > 0;
1686
+ }
1687
+ async function install(source, destination) {
1688
+ await mkdir6(dirname4(destination), { recursive: true });
1689
+ await copyFile2(source, destination);
1690
+ await chmod(destination, (await stat2(source)).mode).catch(() => void 0);
1691
+ }
1692
+ async function pruneEmptyDirectories(target, file) {
1693
+ let directory = dirname4(join9(target, file));
1694
+ while (directory !== target && directory.startsWith(target)) {
1695
+ if ((await readdir(directory)).length > 0) return;
1696
+ await rmdir(directory);
1697
+ directory = dirname4(directory);
1698
+ }
1699
+ }
1700
+ async function updateWorkspace(options) {
1701
+ const target = resolve3(options.targetDir);
1702
+ const runtime = resolveRuntime(options.runtimeDir);
1703
+ if (!isWorkspace(target)) {
1704
+ throw new ValidationError(`No coreslopper workspace found at ${target}. Create one with "npx coreslopper init".`);
1705
+ }
1706
+ if (existsSync6(join9(target, "scripts", "build-runtime.mjs")) || existsSync6(join9(target, "tsup.config.ts"))) {
1707
+ throw new ValidationError('This is the coreslopper source repository. Update it with git, not "coreslopper update".');
1708
+ }
1709
+ const dirtyGit = gitIsDirty(target);
1710
+ const marker = readMarker(target);
1711
+ const fromVersion = marker?.runtimeVersion;
1712
+ const toVersion = runtimeVersion(runtime);
1713
+ if (fromVersion && compareVersions(toVersion, fromVersion) < 0 && !options.force) {
1714
+ throw new ValidationError(
1715
+ `The workspace runs coreslopper ${fromVersion}, newer than ${toVersion}. Use "npx coreslopper@latest update", or pass --force to downgrade.`
1716
+ );
1717
+ }
1718
+ const baseline = marker?.files;
1719
+ const hadBaseline = baseline !== void 0;
1720
+ const base = baseline ?? {};
1721
+ const dryRun = Boolean(options.dryRun);
1722
+ const changes = [];
1723
+ const nextFiles = {};
1724
+ let keptLocal = 0;
1725
+ options.onProgress?.(`Comparing the workspace with coreslopper ${toVersion}...`);
1726
+ const upstream = await hashRuntime(runtime);
1727
+ const record = (path, action) => changes.push({ path, action });
1728
+ const apply = async (path) => {
1729
+ if (!dryRun) await install(join9(runtime, path), join9(target, path));
1730
+ };
1731
+ for (const [path, upstreamHash] of Object.entries(upstream)) {
1732
+ nextFiles[path] = upstreamHash;
1733
+ if (USER_OWNED_FILES.has(path)) continue;
1734
+ const local = join9(target, path);
1735
+ const localExists = existsSync6(local);
1736
+ const baseHash = base[path];
1737
+ if (!localExists) {
1738
+ if (baseHash !== void 0 && !ALWAYS_UPSTREAM_FILES.has(path)) {
1739
+ if (upstreamHash !== baseHash) record(path, "skipped-deleted-locally");
1740
+ continue;
1741
+ }
1742
+ await apply(path);
1743
+ record(path, "added");
1744
+ continue;
1745
+ }
1746
+ const localHash = await hashFile2(local);
1747
+ if (localHash === upstreamHash) continue;
1748
+ if (USER_OWNED_PREFIXES.some((prefix) => path.startsWith(prefix))) continue;
1749
+ if (ALWAYS_UPSTREAM_FILES.has(path) || localHash === baseHash) {
1750
+ await apply(path);
1751
+ record(path, "updated");
1752
+ continue;
1753
+ }
1754
+ if (baseHash !== void 0 && upstreamHash === baseHash) {
1755
+ keptLocal += 1;
1756
+ continue;
1757
+ }
1758
+ if (options.force) {
1759
+ await apply(path);
1760
+ record(path, "overwritten");
1761
+ } else {
1762
+ if (!dryRun) await install(join9(runtime, path), `${local}${CONFLICT_SUFFIX}`);
1763
+ record(path, "conflict");
1764
+ }
1765
+ }
1766
+ for (const [path, baseHash] of Object.entries(base)) {
1767
+ if (path in upstream || USER_OWNED_FILES.has(path)) continue;
1768
+ if (USER_OWNED_PREFIXES.some((prefix) => path.startsWith(prefix))) continue;
1769
+ const local = join9(target, path);
1770
+ if (!existsSync6(local)) continue;
1771
+ if (await hashFile2(local) === baseHash) {
1772
+ if (!dryRun) {
1773
+ await rm(local);
1774
+ await pruneEmptyDirectories(target, path);
1775
+ }
1776
+ record(path, "removed");
1777
+ } else {
1778
+ record(path, "kept-removed-upstream");
1779
+ }
1780
+ }
1781
+ const warnings = [];
1782
+ if (dirtyGit && !dryRun) {
1783
+ warnings.push('The workspace had uncommitted changes before the update; "git diff" now mixes them with the update.');
1784
+ }
1785
+ if (!hadBaseline) {
1786
+ warnings.push(
1787
+ "This workspace has no recorded baseline, so every file that differs from upstream counts as a local change. Review the conflicts, or commit and rerun with --force."
1788
+ );
1789
+ }
1790
+ let installed = false;
1791
+ let uvSynced = false;
1792
+ if (!dryRun) {
1793
+ writeMarkerFile(target, {
1794
+ version: 1,
1795
+ runtimeVersion: toVersion,
1796
+ created: marker?.created ?? (/* @__PURE__ */ new Date()).toISOString(),
1797
+ ...marker?.project ? { project: marker.project } : {},
1798
+ updated: (/* @__PURE__ */ new Date()).toISOString(),
1799
+ files: nextFiles
1800
+ });
1801
+ if (options.install !== false && changes.length > 0) {
1802
+ options.onProgress?.("Installing Node and Python dependencies...");
1803
+ try {
1804
+ const setup = runSetup(target);
1805
+ installed = setup.npm;
1806
+ uvSynced = setup.uv;
1807
+ if (setup.warning) warnings.push(setup.warning);
1808
+ } catch (error) {
1809
+ warnings.push(`Dependency installation failed: ${error instanceof Error ? error.message : String(error)}`);
1810
+ }
1811
+ }
1812
+ }
1813
+ return {
1814
+ targetDir: target,
1815
+ fromVersion,
1816
+ toVersion,
1817
+ hadBaseline,
1818
+ changes,
1819
+ keptLocal,
1820
+ dryRun,
1821
+ installed,
1822
+ uvSynced,
1823
+ dirtyGit,
1824
+ warnings
1825
+ };
1826
+ }
1827
+ var LABELS = {
1828
+ added: "+ added",
1829
+ updated: "~ updated",
1830
+ removed: "- removed",
1831
+ overwritten: "! overwritten",
1832
+ conflict: "! conflict",
1833
+ "kept-removed-upstream": "= kept (removed upstream, changed locally)",
1834
+ "skipped-deleted-locally": "= skipped (deleted locally)"
1835
+ };
1836
+ function printUpdateSummary(result) {
1837
+ const from = result.fromVersion ?? "unknown";
1838
+ const verb = result.dryRun ? "Would update" : "Updated";
1839
+ console.log(`\u2714 ${verb} ${result.targetDir}: coreslopper ${from} \u2192 ${result.toVersion}`);
1840
+ if (result.changes.length === 0) console.log(" Workflow files are already up to date.");
1841
+ for (const change of result.changes) {
1842
+ const suffix = change.action === "conflict" ? ` \u2192 ${change.path}${CONFLICT_SUFFIX}` : "";
1843
+ console.log(` ${LABELS[change.action]}: ${change.path}${suffix}`);
1844
+ }
1845
+ if (result.keptLocal > 0) console.log(` ${result.keptLocal} locally changed file(s) kept; upstream did not change them.`);
1846
+ for (const warning of result.warnings) console.warn(` ! ${warning}`);
1847
+ const conflicts = result.changes.filter((change) => change.action === "conflict").length;
1848
+ if (conflicts > 0) {
1849
+ console.log("");
1850
+ console.log(
1851
+ `${conflicts} file(s) changed both locally and upstream. Merge each "${CONFLICT_SUFFIX}" file into the original, then delete it.`
1852
+ );
1853
+ }
1854
+ if (result.dryRun) {
1855
+ console.log("");
1856
+ console.log("Dry run: nothing was written.");
1857
+ } else if (result.changes.length > 0 && !result.installed) {
1858
+ console.log("");
1859
+ console.log("Next: npm install && npm run python:sync");
1860
+ }
1861
+ }
1862
+
1863
+ // packages/cli/src/init.ts
1602
1864
  function sanitizeSlug(value) {
1603
1865
  const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1604
1866
  return slug || "my-video";
1605
1867
  }
1606
1868
  function defaultProjectSlug(targetDir) {
1607
- const name = basename(resolve3(targetDir));
1608
- return name && name !== "." && name !== sep ? sanitizeSlug(name) : "my-video";
1869
+ const name = basename(resolve4(targetDir));
1870
+ return name && name !== "." && name !== sep2 ? sanitizeSlug(name) : "my-video";
1609
1871
  }
1610
1872
  function upsertEnv(content, key, value) {
1611
1873
  if (!/^[A-Z_][A-Z0-9_]*$/.test(key)) throw new ValidationError(`Invalid environment variable name: ${key}`);
@@ -1618,19 +1880,19 @@ ${line}
1618
1880
  `;
1619
1881
  }
1620
1882
  async function isDirectoryEmpty(path) {
1621
- if (!existsSync6(path)) return true;
1622
- if (!(await stat2(path)).isDirectory()) return false;
1623
- const entries = await readdir(path);
1883
+ if (!existsSync7(path)) return true;
1884
+ if (!(await stat3(path)).isDirectory()) return false;
1885
+ const entries = await readdir2(path);
1624
1886
  return entries.filter((entry) => entry !== ".DS_Store").length === 0;
1625
1887
  }
1626
1888
  function isInside(parent, child) {
1627
1889
  if (parent === child) return true;
1628
- const path = relative2(parent, child);
1890
+ const path = relative3(parent, child);
1629
1891
  return path !== "" && !path.startsWith("..") && !isAbsolute(path);
1630
1892
  }
1631
- function resolveRuntime(options) {
1632
- const runtime = options.runtimeDir ? resolve3(options.runtimeDir) : runtimeDir();
1633
- if (!runtime || !existsSync6(join9(runtime, "templates", "project"))) {
1893
+ function resolveRuntime2(options) {
1894
+ const runtime = options.runtimeDir ? resolve4(options.runtimeDir) : runtimeDir();
1895
+ if (!runtime || !existsSync7(join10(runtime, "templates", "project"))) {
1634
1896
  throw new ValidationError(
1635
1897
  'Runtime assets not found. Use "npx coreslopper init" to create a workspace, or run "npm run build:runtime" in the upstream repository.'
1636
1898
  );
@@ -1638,15 +1900,15 @@ function resolveRuntime(options) {
1638
1900
  return runtime;
1639
1901
  }
1640
1902
  function shouldCopy(source, runtime) {
1641
- const path = relative2(runtime, source);
1903
+ const path = relative3(runtime, source);
1642
1904
  if (!path) return true;
1643
- const segments = path.split(sep);
1905
+ const segments = path.split(sep2);
1644
1906
  if (segments.some((segment) => SKIPPED_DIRECTORIES.has(segment))) return false;
1645
1907
  if (path === ".env" || segments[0] === ".env" && path !== ".env.example") return false;
1646
1908
  return true;
1647
1909
  }
1648
1910
  async function copyRuntime(runtime, target) {
1649
- await mkdir6(target, { recursive: true });
1911
+ await mkdir7(target, { recursive: true });
1650
1912
  await cp2(runtime, target, {
1651
1913
  recursive: true,
1652
1914
  force: true,
@@ -1655,13 +1917,13 @@ async function copyRuntime(runtime, target) {
1655
1917
  });
1656
1918
  }
1657
1919
  async function ensureEnvFile(target, falKey, higgsfieldKey) {
1658
- const envPath = join9(target, ".env");
1920
+ const envPath = join10(target, ".env");
1659
1921
  let content;
1660
- if (existsSync6(envPath)) {
1661
- content = await readFile7(envPath, "utf8");
1922
+ if (existsSync7(envPath)) {
1923
+ content = await readFile8(envPath, "utf8");
1662
1924
  } else {
1663
- const examplePath = join9(target, ".env.example");
1664
- content = existsSync6(examplePath) ? await readFile7(examplePath, "utf8") : "# Local secrets. This file is gitignored.\n";
1925
+ const examplePath = join10(target, ".env.example");
1926
+ content = existsSync7(examplePath) ? await readFile8(examplePath, "utf8") : "# Local secrets. This file is gitignored.\n";
1665
1927
  }
1666
1928
  if (falKey) content = upsertEnv(content, "FAL_KEY", falKey.trim());
1667
1929
  if (!/^FAL_KEY=/m.test(content)) content = `${content.trimEnd()}
@@ -1674,14 +1936,14 @@ HIGGSFIELD_API_KEY=
1674
1936
  `;
1675
1937
  }
1676
1938
  await writeFile6(envPath, content, { encoding: "utf8", mode: 384 });
1677
- await chmod(envPath, 384).catch(() => void 0);
1939
+ await chmod2(envPath, 384).catch(() => void 0);
1678
1940
  }
1679
1941
  function writeMcpConfig(target, suno) {
1680
- const path = join9(target, ".mcp.json");
1942
+ const path = join10(target, ".mcp.json");
1681
1943
  let config = {};
1682
- if (existsSync6(path)) {
1944
+ if (existsSync7(path)) {
1683
1945
  try {
1684
- config = JSON.parse(readFileSync5(path, "utf8"));
1946
+ config = JSON.parse(readFileSync6(path, "utf8"));
1685
1947
  } catch {
1686
1948
  config = {};
1687
1949
  }
@@ -1695,28 +1957,29 @@ function writeMcpConfig(target, suno) {
1695
1957
  writeFileSyncJson(path, config);
1696
1958
  }
1697
1959
  function writeFileSyncJson(path, value) {
1698
- writeFileSync(path, `${JSON.stringify(value, null, 2)}
1960
+ writeFileSync2(path, `${JSON.stringify(value, null, 2)}
1699
1961
  `, "utf8");
1700
1962
  }
1701
- function writeMarker(target, projectSlug) {
1963
+ function writeMarker(target, files, projectSlug) {
1702
1964
  const marker = {
1703
1965
  version: 1,
1704
1966
  runtimeVersion: ownVersion(),
1705
1967
  created: (/* @__PURE__ */ new Date()).toISOString(),
1706
- ...projectSlug ? { project: projectSlug } : {}
1968
+ ...projectSlug ? { project: projectSlug } : {},
1969
+ files
1707
1970
  };
1708
- writeFileSyncJson(join9(target, WORKSPACE_MARKER), marker);
1971
+ writeFileSyncJson(join10(target, WORKSPACE_MARKER), marker);
1709
1972
  }
1710
1973
  function initGit(target) {
1711
- if (existsSync6(join9(target, ".git")) || !commandExists("git")) return false;
1712
- const result = spawnSync2("git", ["init", "-b", "main"], { cwd: target, stdio: "inherit" });
1974
+ if (existsSync7(join10(target, ".git")) || !commandExists("git")) return false;
1975
+ const result = spawnSync3("git", ["init", "-b", "main"], { cwd: target, stdio: "inherit" });
1713
1976
  if (result.status === 0) return true;
1714
- const fallback = spawnSync2("git", ["init"], { cwd: target, stdio: "inherit" });
1977
+ const fallback = spawnSync3("git", ["init"], { cwd: target, stdio: "inherit" });
1715
1978
  return fallback.status === 0;
1716
1979
  }
1717
1980
  async function createFirstProject(target, project, warnings) {
1718
- const projectDir = join9(target, "projects", project.slug);
1719
- if (existsSync6(projectDir)) {
1981
+ const projectDir = join10(target, "projects", project.slug);
1982
+ if (existsSync7(projectDir)) {
1720
1983
  warnings.push(`Project "${project.slug}" already exists; the bundled project was kept.`);
1721
1984
  return void 0;
1722
1985
  }
@@ -1793,9 +2056,9 @@ function ensureTools(target, warnings) {
1793
2056
  }
1794
2057
  }
1795
2058
  async function initWorkspace(options) {
1796
- const target = resolve3(options.targetDir);
1797
- const runtime = resolveRuntime(options);
1798
- if (existsSync6(target) && !(await stat2(target)).isDirectory()) {
2059
+ const target = resolve4(options.targetDir);
2060
+ const runtime = resolveRuntime2(options);
2061
+ if (existsSync7(target) && !(await stat3(target)).isDirectory()) {
1799
2062
  throw new ValidationError(`Target exists and is not a directory: ${target}`);
1800
2063
  }
1801
2064
  if (target === parse(target).root || target === homedir2()) {
@@ -1807,7 +2070,7 @@ async function initWorkspace(options) {
1807
2070
  if (isInside(runtime, target)) {
1808
2071
  throw new ValidationError("The workspace cannot be created inside the coreslopper runtime directory.");
1809
2072
  }
1810
- if (existsSync6(join9(target, "scripts", "build-runtime.mjs")) || existsSync6(join9(target, "tsup.config.ts"))) {
2073
+ if (existsSync7(join10(target, "scripts", "build-runtime.mjs")) || existsSync7(join10(target, "tsup.config.ts"))) {
1811
2074
  throw new ValidationError(
1812
2075
  "This directory is the coreslopper source repository. Create the workspace in a different directory."
1813
2076
  );
@@ -1829,7 +2092,7 @@ async function initWorkspace(options) {
1829
2092
  }
1830
2093
  const gitInitialized = options.git === false ? false : initGit(target);
1831
2094
  const projectSlug = options.project ? await createFirstProject(target, options.project, warnings) : void 0;
1832
- writeMarker(target, projectSlug);
2095
+ writeMarker(target, await hashRuntime(runtime), projectSlug);
1833
2096
  let installed = false;
1834
2097
  let uvSynced = false;
1835
2098
  if (options.install !== false) {
@@ -1852,7 +2115,7 @@ async function initWorkspace(options) {
1852
2115
  gitInitialized,
1853
2116
  warnings
1854
2117
  };
1855
- await writeFile6(join9(target, "GETTING_STARTED.md"), gettingStarted(target, result, options), "utf8");
2118
+ await writeFile6(join10(target, "GETTING_STARTED.md"), gettingStarted(target, result, options), "utf8");
1856
2119
  return result;
1857
2120
  }
1858
2121
 
@@ -1870,7 +2133,7 @@ import {
1870
2133
  spinner,
1871
2134
  text
1872
2135
  } from "@clack/prompts";
1873
- import { resolve as resolve4 } from "path";
2136
+ import { resolve as resolve5 } from "path";
1874
2137
  function unwrap(value) {
1875
2138
  if (isCancel(value)) {
1876
2139
  cancel("Onboarding cancelled.");
@@ -1885,7 +2148,7 @@ function validSlug(value) {
1885
2148
  return void 0;
1886
2149
  }
1887
2150
  async function chooseTarget(options) {
1888
- if (options.dir) return resolve4(options.dir);
2151
+ if (options.dir) return resolve5(options.dir);
1889
2152
  if (await isDirectoryEmpty(process.cwd())) return process.cwd();
1890
2153
  const answer = unwrap(
1891
2154
  await text({
@@ -1894,7 +2157,7 @@ async function chooseTarget(options) {
1894
2157
  defaultValue: "coreslopper-studio"
1895
2158
  })
1896
2159
  );
1897
- return resolve4(answer);
2160
+ return resolve5(answer);
1898
2161
  }
1899
2162
  async function ensureTargetIsUsable(target, force = false) {
1900
2163
  if (await isDirectoryEmpty(target)) return;
@@ -1921,24 +2184,24 @@ async function offerTools(target, enabled) {
1921
2184
  }
1922
2185
  let uvReady = Boolean(checks.find((tool) => tool.id === "uv")?.ok);
1923
2186
  if (!uvReady && missing.some((tool) => tool.id === "uv")) {
1924
- const install = unwrap(
2187
+ const install2 = unwrap(
1925
2188
  await confirm({
1926
2189
  message: "Install uv? It manages the Python audio-analysis environment.",
1927
2190
  initialValue: true
1928
2191
  })
1929
2192
  );
1930
- if (install) uvReady = installUv();
2193
+ if (install2) uvReady = installUv();
1931
2194
  if (!uvReady) log.warn("uv was not installed. Audio analysis will need it later.");
1932
2195
  }
1933
2196
  const pythonReady = Boolean(checks.find((tool) => tool.id === "python")?.ok);
1934
2197
  if (!pythonReady && uvReady) {
1935
- const install = unwrap(
2198
+ const install2 = unwrap(
1936
2199
  await confirm({
1937
2200
  message: "Install Python 3.12 with uv?",
1938
2201
  initialValue: true
1939
2202
  })
1940
2203
  );
1941
- if (install && !installPython()) {
2204
+ if (install2 && !installPython()) {
1942
2205
  log.warn("Python 3.12 was not installed. Audio analysis will need it later.");
1943
2206
  }
1944
2207
  }
@@ -1952,13 +2215,13 @@ async function offerTools(target, enabled) {
1952
2215
  );
1953
2216
  }
1954
2217
  if (missing.some((tool) => tool.id === "claude")) {
1955
- const install = unwrap(
2218
+ const install2 = unwrap(
1956
2219
  await confirm({
1957
2220
  message: "Install Claude Code globally? The /video-next pipeline runs there.",
1958
2221
  initialValue: true
1959
2222
  })
1960
2223
  );
1961
- if (install && !installClaudeCode()) {
2224
+ if (install2 && !installClaudeCode()) {
1962
2225
  log.warn('Claude Code was not installed. Install it with "npm install -g @anthropic-ai/claude-code".');
1963
2226
  }
1964
2227
  }
@@ -2130,7 +2393,7 @@ async function onboard(options = {}) {
2130
2393
  if (slugError) throw new ValidationError(slugError);
2131
2394
  project = { slug: projectSlug, title: options.title, budget, song, image, video };
2132
2395
  }
2133
- const install = options.install ?? unwrap(
2396
+ const install2 = options.install ?? unwrap(
2134
2397
  await confirm({
2135
2398
  message: "Install Node and Python dependencies now? (takes a few minutes)",
2136
2399
  initialValue: true
@@ -2148,7 +2411,7 @@ async function onboard(options = {}) {
2148
2411
  `Song: ${project ? project.song : "-"}`,
2149
2412
  `Visuals: ${project ? project.image === project.video ? project.image : `image=${project.image}, video=${project.video}` : "-"}`,
2150
2413
  `Budget: ${project ? `$${project.budget.toFixed(2)}` : "-"}`,
2151
- `Install: ${install ? "yes" : "no"}`,
2414
+ `Install: ${install2 ? "yes" : "no"}`,
2152
2415
  `Git: ${git ? "yes" : "no"}`
2153
2416
  ].join("\n");
2154
2417
  note(summary, "Ready to create");
@@ -2165,7 +2428,7 @@ async function onboard(options = {}) {
2165
2428
  const result = await initWorkspace({
2166
2429
  targetDir: target,
2167
2430
  force: true,
2168
- install,
2431
+ install: install2,
2169
2432
  git,
2170
2433
  project,
2171
2434
  falKey,
@@ -2178,7 +2441,7 @@ async function onboard(options = {}) {
2178
2441
  if (!installFfmpeg(target)) log.warn("ffmpeg was not installed. Install it later for footage and loudness.");
2179
2442
  }
2180
2443
  for (const warning of result.warnings) log.warn(warning);
2181
- if (!result.installed && install) {
2444
+ if (!result.installed && install2) {
2182
2445
  log.error("Dependency installation did not finish. Run npm install in the workspace.");
2183
2446
  process.exitCode = 1;
2184
2447
  }
@@ -2234,7 +2497,7 @@ async function runInitCommand(targetArg, options) {
2234
2497
  });
2235
2498
  return;
2236
2499
  }
2237
- const target = resolve5(targetArg ?? ".");
2500
+ const target = resolve6(targetArg ?? ".");
2238
2501
  const visual = options.visual ?? "mock";
2239
2502
  const image = options.imageProvider ?? visual;
2240
2503
  const video = options.videoProvider ?? visual;
@@ -2288,7 +2551,7 @@ async function runInitCommand(targetArg, options) {
2288
2551
  }
2289
2552
 
2290
2553
  // packages/cli/src/status.ts
2291
- import { existsSync as existsSync7 } from "fs";
2554
+ import { existsSync as existsSync8 } from "fs";
2292
2555
  async function printStatus(projectDir) {
2293
2556
  const loaded = await loadProject(projectDir);
2294
2557
  const state = loaded.state;
@@ -2300,11 +2563,11 @@ async function printStatus(projectDir) {
2300
2563
  );
2301
2564
  const next = nextStage(state);
2302
2565
  console.log(`Next: ${next ? `${next.id} (${next.skills.join(", ")})` : "done"}`);
2303
- if (!existsSync7(loaded.paths.state)) console.log("Note: state.json does not exist yet; showing the initial state.");
2566
+ if (!existsSync8(loaded.paths.state)) console.log("Note: state.json does not exist yet; showing the initial state.");
2304
2567
  }
2305
2568
 
2306
2569
  // packages/cli/src/utils.ts
2307
- import { resolve as resolve6 } from "path";
2570
+ import { resolve as resolve7 } from "path";
2308
2571
  import { ZodError } from "zod";
2309
2572
  function printError(error) {
2310
2573
  if (error instanceof ValidationError) {
@@ -2400,13 +2663,13 @@ function workspaceSummary(root) {
2400
2663
  console.log(`Projects: ${projects.join(", ")}`);
2401
2664
  try {
2402
2665
  const project = resolveProjectArg(root);
2403
- console.log(`Tip: npx coreslopper status ${relative3(root, project)}`);
2666
+ console.log(`Tip: npx coreslopper status ${relative4(root, project)}`);
2404
2667
  } catch {
2405
2668
  }
2406
2669
  } else {
2407
2670
  console.log("No projects yet. Create one with: npx coreslopper new <slug> --budget 50");
2408
2671
  }
2409
- console.log("Commands: init, doctor, new, status, validate");
2672
+ console.log("Commands: init, update, doctor, new, status, validate");
2410
2673
  }
2411
2674
  var program = new Command();
2412
2675
  program.name("coreslopper").description("Create and manage a coreslopper music-video workspace.").version(ownVersion()).showHelpAfterError();
@@ -2424,9 +2687,23 @@ program.command("init [dir]").description("Create a workspace, install tools, an
2424
2687
  await runInitCommand(dir, options);
2425
2688
  })
2426
2689
  );
2690
+ program.command("update [dir]").description("Update the workflow files of a workspace to this coreslopper version; keeps local changes.").option("--dry-run", "Show what would change without writing anything").option("--force", "Overwrite locally changed files and allow a downgrade").option("--no-install", "Skip npm install and uv sync").option("--runtime <dir>", "Runtime directory (development only)").action(
2691
+ (dir, options) => run(async () => {
2692
+ const target = dir ? resolve8(dir) : requireWorkspaceRoot();
2693
+ const result = await updateWorkspace({
2694
+ targetDir: target,
2695
+ runtimeDir: options.runtime,
2696
+ dryRun: options.dryRun,
2697
+ force: options.force,
2698
+ install: options.install,
2699
+ onProgress: (message) => console.log(message)
2700
+ });
2701
+ printUpdateSummary(result);
2702
+ })
2703
+ );
2427
2704
  program.command("doctor [dir]").description("Check the workspace, tools, and credentials.").option("--json", "Print machine-readable JSON").action(
2428
2705
  (dir, options) => run(async () => {
2429
- const root = dir ? resolve7(dir) : findWorkspaceRoot() ?? process.cwd();
2706
+ const root = dir ? resolve8(dir) : findWorkspaceRoot() ?? process.cwd();
2430
2707
  const result = await runDoctor(root, { json: options.json });
2431
2708
  if (!result.ok) process.exitCode = 1;
2432
2709
  })