coreslopper 0.1.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/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/dist/cli.js +340 -63
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/runtime/.github/workflows/ci.yml +8 -6
- package/runtime/.github/workflows/release.yml +3 -3
- package/runtime/.release-please-manifest.json +1 -1
- package/runtime/AGENTS.md +2 -1
- package/runtime/CHANGELOG.md +8 -0
- package/runtime/README.md +1 -1
- package/runtime/docs/adr/0010-npx-onboarding.md +1 -0
- package/runtime/docs/adr/0012-workspace-update.md +27 -0
- package/runtime/docs/adr/README.md +1 -0
- package/runtime/docs/glossary.md +5 -1
- package/runtime/docs/onboarding.md +26 -5
- package/runtime/package-lock.json +2 -2
- package/runtime/package.json +1 -1
- package/runtime/packages/cli/src/init.ts +4 -16
- package/runtime/packages/cli/src/npx.ts +28 -1
- package/runtime/packages/cli/src/update.ts +372 -0
- package/runtime/packages/cli/src/workspace.ts +4 -0
- package/runtime/tests/update.test.ts +185 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0](https://github.com/hemi1986/coreslopper/compare/v0.1.0...v0.2.0) (2026-11-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add `coreslopper update` to merge a newer workflow into a workspace ([3eddb5c](https://github.com/hemi1986/coreslopper/commit/3eddb5caa64a534a9bf21ca13d45d24c747976e3))
|
|
9
|
+
* add `coreslopper update` to merge a newer workflow into a workspace ([1d10ffd](https://github.com/hemi1986/coreslopper/commit/1d10ffda2a6639d1a5b2a489564a54c9b2422cda))
|
|
10
|
+
|
|
3
11
|
## 0.1.0 (2026-11-08)
|
|
4
12
|
|
|
5
13
|
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ cd my-video
|
|
|
72
72
|
claude
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
Run `/mcp` once to authenticate Suno, then `/video-next my-video`. The generated workspace contains the full workflow runtime, so every `npm run cli -- ...` command in this README works inside it. Running `npx coreslopper` without arguments inside a workspace prints the available commands. See [docs/onboarding.md](docs/onboarding.md) for all flags and troubleshooting.
|
|
75
|
+
Run `/mcp` once to authenticate Suno, then `/video-next my-video`. The generated workspace contains the full workflow runtime, so every `npm run cli -- ...` command in this README works inside it. Running `npx coreslopper` without arguments inside a workspace prints the available commands. To move a workspace to a newer release, run `npx coreslopper@latest update` inside it; local edits are kept. See [docs/onboarding.md](docs/onboarding.md) for all flags, updating, and troubleshooting.
|
|
76
76
|
|
|
77
77
|
## Quickstarts
|
|
78
78
|
|
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
|
|
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
|
|
1467
|
+
import { resolve as resolve6 } from "path";
|
|
1468
1468
|
|
|
1469
1469
|
// packages/cli/src/init.ts
|
|
1470
|
-
import { spawnSync as
|
|
1471
|
-
import { existsSync as
|
|
1472
|
-
import { chmod, cp as cp2, mkdir as
|
|
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
|
|
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/
|
|
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(
|
|
1608
|
-
return name && name !== "." && name !==
|
|
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 (!
|
|
1622
|
-
if (!(await
|
|
1623
|
-
const entries = await
|
|
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 =
|
|
1890
|
+
const path = relative3(parent, child);
|
|
1629
1891
|
return path !== "" && !path.startsWith("..") && !isAbsolute(path);
|
|
1630
1892
|
}
|
|
1631
|
-
function
|
|
1632
|
-
const runtime = options.runtimeDir ?
|
|
1633
|
-
if (!runtime || !
|
|
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 =
|
|
1903
|
+
const path = relative3(runtime, source);
|
|
1642
1904
|
if (!path) return true;
|
|
1643
|
-
const segments = path.split(
|
|
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
|
|
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 =
|
|
1920
|
+
const envPath = join10(target, ".env");
|
|
1659
1921
|
let content;
|
|
1660
|
-
if (
|
|
1661
|
-
content = await
|
|
1922
|
+
if (existsSync7(envPath)) {
|
|
1923
|
+
content = await readFile8(envPath, "utf8");
|
|
1662
1924
|
} else {
|
|
1663
|
-
const examplePath =
|
|
1664
|
-
content =
|
|
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
|
|
1939
|
+
await chmod2(envPath, 384).catch(() => void 0);
|
|
1678
1940
|
}
|
|
1679
1941
|
function writeMcpConfig(target, suno) {
|
|
1680
|
-
const path =
|
|
1942
|
+
const path = join10(target, ".mcp.json");
|
|
1681
1943
|
let config = {};
|
|
1682
|
-
if (
|
|
1944
|
+
if (existsSync7(path)) {
|
|
1683
1945
|
try {
|
|
1684
|
-
config = JSON.parse(
|
|
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
|
-
|
|
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(
|
|
1971
|
+
writeFileSyncJson(join10(target, WORKSPACE_MARKER), marker);
|
|
1709
1972
|
}
|
|
1710
1973
|
function initGit(target) {
|
|
1711
|
-
if (
|
|
1712
|
-
const result =
|
|
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 =
|
|
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 =
|
|
1719
|
-
if (
|
|
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 =
|
|
1797
|
-
const runtime =
|
|
1798
|
-
if (
|
|
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 (
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
|
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: ${
|
|
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 &&
|
|
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 =
|
|
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
|
|
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 (!
|
|
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
|
|
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 ${
|
|
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 ?
|
|
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
|
})
|