arcane-cli 0.16.1 → 0.16.3
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.
|
@@ -220,6 +220,13 @@ Create the session branch as the **first action** of any session, before any fil
|
|
|
220
220
|
1. `git branch -m <old> <new>`
|
|
221
221
|
2. `git push -u origin <new>`
|
|
222
222
|
3. `git push origin --delete <old>` (skip this if an active PR still depends on `<old>`).
|
|
223
|
+
- **EF-20 hazard:** on some Windows/Git-for-Windows filesystems, a rename or delete can trigger
|
|
224
|
+
an interactive retry prompt (e.g. a file lock from an editor or antivirus scan) that blocks
|
|
225
|
+
indefinitely on a terminal with no non-interactive fallback. When running these commands
|
|
226
|
+
directly through a tool that supports a per-call timeout (an agent's shell tool, a CI step),
|
|
227
|
+
set one rather than assuming the command will return. Arcane's own TypeScript Git helper
|
|
228
|
+
(`src/modules/git.ts`) applies this as a standing contract — closed stdin plus a
|
|
229
|
+
command-class timeout — for every `git` invocation the CLI itself makes.
|
|
223
230
|
|
|
224
231
|
**Agents:**
|
|
225
232
|
|
|
@@ -101,7 +101,8 @@ Rules:
|
|
|
101
101
|
|
|
102
102
|
9. **Stage and commit session-close docs.**
|
|
103
103
|
|
|
104
|
-
**Remote-capability check:** classify the state created by `spell-open-session` before branching, staging, push, or PR operations.
|
|
104
|
+
**Remote-capability check:** classify the state created by `spell-open-session` before branching, staging, push, or PR operations. Check for **not a repository at all** first — every other classification assumes a `.git` directory exists.
|
|
105
|
+
- **Not a Git repository (EF-05):** `git rev-parse --is-inside-work-tree` fails. Do not attempt any git operation (branch, add, commit, push). Fail closed with: `No Git repository detected. Run \`git init -b main\` first (explicit -b avoids landing on "master" on systems where that's the init.defaultBranch default), then re-run spell-close-session.` This mirrors `spell init`'s own next-steps guidance for the same state.
|
|
105
106
|
- **Usable supported remote/merge path:** requires an authenticated supported provider (`github.com`, `dev.azure.com`, or `visualstudio.com`). Keep the current compliant session/PR/worktree branch, resolve the actual remote name and integration branch, and use the provider-neutral PR path below.
|
|
106
107
|
- **No usable remote/merge path:** remain on the current trunk. Do not create a dead session branch, push, pull, or open a PR. Apply the EF-28 interactive commit approval gate, commit locally on trunk after approval, report `Local-only close: committed on <trunk>; no remote PR/pull performed`, and skip steps 9a, 9c, and 10's remote operations.
|
|
107
108
|
- **Read-only session:** if no repository mutation occurred, do not create a branch or commit.
|
package/dist/index.js
CHANGED
|
@@ -1599,15 +1599,140 @@ async function runAgentsList(targetDir) {
|
|
|
1599
1599
|
|
|
1600
1600
|
// src/modules/git.ts
|
|
1601
1601
|
import { execFile } from "child_process";
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1602
|
+
var GitTimeoutError = class extends Error {
|
|
1603
|
+
constructor(message, options) {
|
|
1604
|
+
super(message, options);
|
|
1605
|
+
this.name = "GitTimeoutError";
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
var NETWORK_SUBCOMMANDS = /* @__PURE__ */ new Set(["fetch", "pull", "push", "clone", "ls-remote", "send-email"]);
|
|
1609
|
+
var WRITE_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
1610
|
+
"init",
|
|
1611
|
+
"commit",
|
|
1612
|
+
"add",
|
|
1613
|
+
"checkout",
|
|
1614
|
+
"switch",
|
|
1615
|
+
"merge",
|
|
1616
|
+
"rebase",
|
|
1617
|
+
"reset",
|
|
1618
|
+
"tag",
|
|
1619
|
+
"mv",
|
|
1620
|
+
"rm",
|
|
1621
|
+
"cherry-pick",
|
|
1622
|
+
"revert",
|
|
1623
|
+
"stash",
|
|
1624
|
+
"apply",
|
|
1625
|
+
"am",
|
|
1626
|
+
"restore",
|
|
1627
|
+
"submodule",
|
|
1628
|
+
"sparse-checkout",
|
|
1629
|
+
"maintenance",
|
|
1630
|
+
"notes"
|
|
1631
|
+
]);
|
|
1632
|
+
var AMBIGUOUS_AS_WRITE = /* @__PURE__ */ new Set(["branch", "config", "remote", "worktree"]);
|
|
1633
|
+
var READ_SUBCOMMANDS = /* @__PURE__ */ new Set([
|
|
1634
|
+
"status",
|
|
1635
|
+
"rev-parse",
|
|
1636
|
+
"log",
|
|
1637
|
+
"diff",
|
|
1638
|
+
"show",
|
|
1639
|
+
"describe",
|
|
1640
|
+
"cat-file",
|
|
1641
|
+
"ls-files",
|
|
1642
|
+
"ls-tree",
|
|
1643
|
+
"symbolic-ref",
|
|
1644
|
+
"merge-base",
|
|
1645
|
+
"blame",
|
|
1646
|
+
"shortlog",
|
|
1647
|
+
"grep",
|
|
1648
|
+
"reflog",
|
|
1649
|
+
"diff-tree",
|
|
1650
|
+
"rev-list",
|
|
1651
|
+
"name-rev",
|
|
1652
|
+
"count-objects",
|
|
1653
|
+
"verify-commit",
|
|
1654
|
+
"verify-tag",
|
|
1655
|
+
"help",
|
|
1656
|
+
"version"
|
|
1657
|
+
]);
|
|
1658
|
+
var GLOBAL_FLAGS_WITH_SEPARATE_VALUE = /* @__PURE__ */ new Set(["-c", "-C"]);
|
|
1659
|
+
var DEFAULT_TIMEOUTS_MS = {
|
|
1660
|
+
read: 15e3,
|
|
1661
|
+
write: 3e4,
|
|
1662
|
+
network: 12e4
|
|
1663
|
+
};
|
|
1664
|
+
function classifyGitCommand(args) {
|
|
1665
|
+
let subcommand;
|
|
1666
|
+
for (let i = 0; i < args.length; i++) {
|
|
1667
|
+
const arg = args[i];
|
|
1668
|
+
if (GLOBAL_FLAGS_WITH_SEPARATE_VALUE.has(arg)) {
|
|
1669
|
+
i++;
|
|
1670
|
+
continue;
|
|
1671
|
+
}
|
|
1672
|
+
if (arg.startsWith("-")) continue;
|
|
1673
|
+
subcommand = arg;
|
|
1674
|
+
break;
|
|
1675
|
+
}
|
|
1676
|
+
if (!subcommand) return "read";
|
|
1677
|
+
if (NETWORK_SUBCOMMANDS.has(subcommand)) return "network";
|
|
1678
|
+
if (WRITE_SUBCOMMANDS.has(subcommand) || AMBIGUOUS_AS_WRITE.has(subcommand)) return "write";
|
|
1679
|
+
if (READ_SUBCOMMANDS.has(subcommand)) return "read";
|
|
1680
|
+
return "write";
|
|
1681
|
+
}
|
|
1682
|
+
function buildGitEnv() {
|
|
1683
|
+
return {
|
|
1684
|
+
...process.env,
|
|
1685
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
1686
|
+
GCM_INTERACTIVE: "Never",
|
|
1687
|
+
GIT_OPTIONAL_LOCKS: "0"
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
function runGit(cwd, args, options = {}) {
|
|
1691
|
+
const commandClass = options.commandClass ?? classifyGitCommand(args);
|
|
1692
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUTS_MS[commandClass] ?? DEFAULT_TIMEOUTS_MS.write;
|
|
1693
|
+
return new Promise((resolve, reject) => {
|
|
1694
|
+
const child = execFile(
|
|
1607
1695
|
"git",
|
|
1608
|
-
|
|
1609
|
-
{
|
|
1696
|
+
args,
|
|
1697
|
+
{
|
|
1698
|
+
cwd,
|
|
1699
|
+
env: buildGitEnv(),
|
|
1700
|
+
timeout: timeoutMs,
|
|
1701
|
+
// SIGTERM (Node's default) rather than SIGKILL: gives git a chance
|
|
1702
|
+
// to run its own cleanup on POSIX, relevant to EF-20's own concern
|
|
1703
|
+
// about a killed operation leaving refs/locks in an uncertain state.
|
|
1704
|
+
// Moot on Windows -- Node docs: both map to an abrupt TerminateProcess
|
|
1705
|
+
// there -- but this module isn't Windows-gated.
|
|
1706
|
+
killSignal: "SIGTERM",
|
|
1707
|
+
windowsHide: true,
|
|
1708
|
+
encoding: "utf8"
|
|
1709
|
+
},
|
|
1710
|
+
(error, stdout, stderr) => {
|
|
1711
|
+
if (error) {
|
|
1712
|
+
if (error.killed) {
|
|
1713
|
+
reject(
|
|
1714
|
+
new GitTimeoutError(
|
|
1715
|
+
// NOTE: embeds the raw args, including any secrets a future
|
|
1716
|
+
// network-class caller might pass (e.g. a credentialed
|
|
1717
|
+
// clone URL). No current caller does; redact before one does.
|
|
1718
|
+
`git ${args.join(" ")} timed out after ${timeoutMs}ms (class: ${commandClass})`,
|
|
1719
|
+
{ cause: error }
|
|
1720
|
+
)
|
|
1721
|
+
);
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
reject(error);
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
resolve({ stdout, stderr });
|
|
1728
|
+
}
|
|
1610
1729
|
);
|
|
1730
|
+
child.stdin?.end();
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
async function inspectGitRepository(cwd) {
|
|
1734
|
+
try {
|
|
1735
|
+
const { stdout: stdout2 } = await runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
1611
1736
|
if (stdout2.trim() !== "true") {
|
|
1612
1737
|
return { status: "not-repository" };
|
|
1613
1738
|
}
|
|
@@ -1615,28 +1740,61 @@ async function inspectGitRepository(cwd) {
|
|
|
1615
1740
|
return { status: "not-repository" };
|
|
1616
1741
|
}
|
|
1617
1742
|
try {
|
|
1618
|
-
await
|
|
1743
|
+
await runGit(cwd, ["rev-parse", "--verify", "HEAD"]);
|
|
1619
1744
|
} catch {
|
|
1620
1745
|
return { status: "no-commits" };
|
|
1621
1746
|
}
|
|
1622
|
-
const { stdout } = await
|
|
1623
|
-
"git",
|
|
1624
|
-
["status", "--porcelain", "-uall"],
|
|
1625
|
-
{ cwd }
|
|
1626
|
-
);
|
|
1747
|
+
const { stdout } = await runGit(cwd, ["status", "--porcelain", "-uall"]);
|
|
1627
1748
|
const lines = stdout.trim();
|
|
1628
1749
|
return {
|
|
1629
1750
|
status: "ready",
|
|
1630
1751
|
uncommittedChanges: lines.length === 0 ? 0 : lines.split("\n").length
|
|
1631
1752
|
};
|
|
1632
1753
|
}
|
|
1633
|
-
async function
|
|
1754
|
+
async function correctUnbornMasterDefault(cwd) {
|
|
1755
|
+
let current;
|
|
1756
|
+
try {
|
|
1757
|
+
const { stdout } = await runGit(cwd, ["symbolic-ref", "--short", "HEAD"]);
|
|
1758
|
+
current = stdout.trim();
|
|
1759
|
+
} catch {
|
|
1760
|
+
return { corrected: false, to: "main" };
|
|
1761
|
+
}
|
|
1762
|
+
if (current !== "master") {
|
|
1763
|
+
return { corrected: false, to: "main" };
|
|
1764
|
+
}
|
|
1765
|
+
try {
|
|
1766
|
+
await runGit(cwd, ["rev-parse", "--verify", "HEAD"]);
|
|
1767
|
+
return { corrected: false, to: "main" };
|
|
1768
|
+
} catch {
|
|
1769
|
+
}
|
|
1634
1770
|
try {
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
return lines.length === 0 ? 0 : lines.split("\n").length;
|
|
1771
|
+
await runGit(cwd, ["rev-parse", "--verify", "refs/heads/main"]);
|
|
1772
|
+
return { corrected: false, to: "main" };
|
|
1638
1773
|
} catch {
|
|
1639
|
-
|
|
1774
|
+
}
|
|
1775
|
+
await runGit(cwd, ["symbolic-ref", "HEAD", "refs/heads/main"]);
|
|
1776
|
+
return { corrected: true, from: current, to: "main" };
|
|
1777
|
+
}
|
|
1778
|
+
async function ensureLocalPullRebase(cwd) {
|
|
1779
|
+
let rawLocalValue;
|
|
1780
|
+
try {
|
|
1781
|
+
const { stdout } = await runGit(cwd, ["config", "--local", "--get", "pull.rebase"]);
|
|
1782
|
+
rawLocalValue = stdout.trim();
|
|
1783
|
+
} catch {
|
|
1784
|
+
rawLocalValue = void 0;
|
|
1785
|
+
}
|
|
1786
|
+
if (rawLocalValue === void 0) {
|
|
1787
|
+
await runGit(cwd, ["config", "--local", "pull.rebase", "true"]);
|
|
1788
|
+
return { action: "set" };
|
|
1789
|
+
}
|
|
1790
|
+
try {
|
|
1791
|
+
const { stdout } = await runGit(cwd, ["config", "--local", "--type=bool", "--get", "pull.rebase"]);
|
|
1792
|
+
if (stdout.trim() === "false") {
|
|
1793
|
+
return { action: "explicit-false-preserved" };
|
|
1794
|
+
}
|
|
1795
|
+
return { action: "already-set" };
|
|
1796
|
+
} catch {
|
|
1797
|
+
return { action: "already-set" };
|
|
1640
1798
|
}
|
|
1641
1799
|
}
|
|
1642
1800
|
|
|
@@ -1716,21 +1874,42 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
1716
1874
|
console.log(chalk2.hex("#a855f7")(`
|
|
1717
1875
|
\u2726 Arcane v${packageVersion}
|
|
1718
1876
|
`));
|
|
1877
|
+
let repoNotFound = false;
|
|
1719
1878
|
if (!options.dryRun) {
|
|
1720
|
-
const
|
|
1721
|
-
if (
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1879
|
+
const gitState = await inspectGitRepository(targetDir);
|
|
1880
|
+
if (gitState.status === "not-repository") {
|
|
1881
|
+
repoNotFound = true;
|
|
1882
|
+
} else {
|
|
1883
|
+
if (gitState.status === "no-commits") {
|
|
1884
|
+
const correction = await correctUnbornMasterDefault(targetDir);
|
|
1885
|
+
if (correction.corrected) {
|
|
1886
|
+
printInfo(
|
|
1887
|
+
`Repointed the unborn branch from "${correction.from}" to "${correction.to}" (avoids the Git for Windows default-branch trap).`
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
if (gitState.status === "ready" && gitState.uncommittedChanges > 0) {
|
|
1892
|
+
printWarning(
|
|
1893
|
+
`This repo has ${gitState.uncommittedChanges} uncommitted change${gitState.uncommittedChanges === 1 ? "" : "s"}.`
|
|
1894
|
+
);
|
|
1895
|
+
printInfo("Running spell init will add new files to your working tree.");
|
|
1896
|
+
console.log();
|
|
1897
|
+
const continueAnyway = await confirm({
|
|
1898
|
+
message: "Continue anyway?",
|
|
1899
|
+
default: true
|
|
1900
|
+
});
|
|
1901
|
+
if (!continueAnyway) {
|
|
1902
|
+
console.log("\nCancelled. Commit or stash your changes first.");
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
console.log();
|
|
1906
|
+
}
|
|
1907
|
+
const rebaseResult = await ensureLocalPullRebase(targetDir);
|
|
1908
|
+
if (rebaseResult.action === "explicit-false-preserved") {
|
|
1909
|
+
printWarning(
|
|
1910
|
+
"This repo's pull.rebase is explicitly set to false, but Arcane's governance mandates rebase-and-fast-forward (see .arcane/governance/git-conventions.md). Left as-is since you set it explicitly."
|
|
1911
|
+
);
|
|
1732
1912
|
}
|
|
1733
|
-
console.log();
|
|
1734
1913
|
}
|
|
1735
1914
|
}
|
|
1736
1915
|
let profile = options.profile;
|
|
@@ -1879,6 +2058,12 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
1879
2058
|
"Review .arcane/governance/ docs and customize for your project"
|
|
1880
2059
|
);
|
|
1881
2060
|
}
|
|
2061
|
+
if (repoNotFound) {
|
|
2062
|
+
printNextStep(
|
|
2063
|
+
step++,
|
|
2064
|
+
'Initialize Git for this project: `git init -b main` (avoids defaulting to "master" on systems where that\'s the system default)'
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
1882
2067
|
printNextStep(
|
|
1883
2068
|
step++,
|
|
1884
2069
|
"Commit the new files: git add .arcane .github .claude AGENTS.md CLAUDE.md .arcane.json"
|
|
@@ -2391,7 +2576,7 @@ async function runUninstall(options, targetDir) {
|
|
|
2391
2576
|
import { access as access3, readFile as readFile5, mkdir as mkdir5, copyFile as fsCopyFile2 } from "fs/promises";
|
|
2392
2577
|
import { join as join10, dirname as dirname2 } from "path";
|
|
2393
2578
|
import { execFile as execFile2 } from "child_process";
|
|
2394
|
-
import { promisify
|
|
2579
|
+
import { promisify } from "util";
|
|
2395
2580
|
|
|
2396
2581
|
// src/config/incidents.ts
|
|
2397
2582
|
var INCIDENT_QUEUE = [
|
|
@@ -2459,7 +2644,7 @@ function evaluateIncidentGate(records) {
|
|
|
2459
2644
|
}
|
|
2460
2645
|
|
|
2461
2646
|
// src/commands/doctor.ts
|
|
2462
|
-
var
|
|
2647
|
+
var execFileAsync = promisify(execFile2);
|
|
2463
2648
|
async function checkNodeVersion() {
|
|
2464
2649
|
const name = "Node.js version";
|
|
2465
2650
|
const raw = process.version;
|
|
@@ -2497,7 +2682,7 @@ async function checkVSCodeExtension(id, label) {
|
|
|
2497
2682
|
let installBin = "code";
|
|
2498
2683
|
for (const bin of ["code", "code-insiders"]) {
|
|
2499
2684
|
try {
|
|
2500
|
-
await
|
|
2685
|
+
await execFileAsync(bin, ["--version"]);
|
|
2501
2686
|
installBin = bin;
|
|
2502
2687
|
break;
|
|
2503
2688
|
} catch {
|
|
@@ -2559,6 +2744,29 @@ async function checkArcaneManifest(targetDir) {
|
|
|
2559
2744
|
};
|
|
2560
2745
|
}
|
|
2561
2746
|
}
|
|
2747
|
+
async function checkPullRebase(targetDir) {
|
|
2748
|
+
const name = "Git pull.rebase (rebase-and-fast-forward mandate)";
|
|
2749
|
+
try {
|
|
2750
|
+
const { stdout } = await runGit(targetDir, ["config", "--type=bool", "--get", "pull.rebase"]);
|
|
2751
|
+
const value = stdout.trim();
|
|
2752
|
+
if (value === "true") {
|
|
2753
|
+
return { name, passed: true, message: "pull.rebase=true" };
|
|
2754
|
+
}
|
|
2755
|
+
return {
|
|
2756
|
+
name,
|
|
2757
|
+
passed: false,
|
|
2758
|
+
message: `pull.rebase resolves to "${value}", not "true" -- a bare \`git pull\` will create merge commits, contradicting git-conventions.md's rebase-and-fast-forward mandate. Run: git config --local pull.rebase true`,
|
|
2759
|
+
blocking: false
|
|
2760
|
+
};
|
|
2761
|
+
} catch {
|
|
2762
|
+
return {
|
|
2763
|
+
name,
|
|
2764
|
+
passed: false,
|
|
2765
|
+
message: "pull.rebase is unset, not a recognized boolean, or this isn't a Git repository -- effective behavior may not be rebase-and-fast-forward, and Git for Windows defaults this to false when unset. Run: git config --local pull.rebase true",
|
|
2766
|
+
blocking: false
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2562
2770
|
function checkIncidentReleaseGate() {
|
|
2563
2771
|
const result = evaluateIncidentGate(INCIDENT_QUEUE);
|
|
2564
2772
|
if (result.blocked) {
|
|
@@ -2621,7 +2829,8 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
|
2621
2829
|
checkNodeVersion(),
|
|
2622
2830
|
checkVSCodeExtension("GitHub.copilot-chat", "GitHub Copilot (Chat)"),
|
|
2623
2831
|
checkArcaneManifest(targetDir),
|
|
2624
|
-
Promise.resolve(checkIncidentReleaseGate())
|
|
2832
|
+
Promise.resolve(checkIncidentReleaseGate()),
|
|
2833
|
+
checkPullRebase(targetDir)
|
|
2625
2834
|
]);
|
|
2626
2835
|
const sessionResults = await checkSessionContinuity(targetDir);
|
|
2627
2836
|
results.push(...sessionResults);
|