arcane-cli 0.20.0 → 0.20.2
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/index.js +403 -96
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ const require = __createRequire(import.meta.url);
|
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { createRequire as createRequire2 } from "module";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
|
-
import { dirname as
|
|
9
|
+
import { dirname as dirname4, join as join12 } from "path";
|
|
10
10
|
|
|
11
11
|
// src/commands/init.ts
|
|
12
12
|
import { select as select2, confirm, input as input2 } from "@inquirer/prompts";
|
|
@@ -672,7 +672,7 @@ var WORDMARK = [
|
|
|
672
672
|
var INDENT = " ";
|
|
673
673
|
var LOCKUP_WIDTH = INDENT.length + Math.max(...WORDMARK.map((l) => l.length));
|
|
674
674
|
function sleep(ms) {
|
|
675
|
-
return new Promise((
|
|
675
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
676
676
|
}
|
|
677
677
|
function isAnimatable() {
|
|
678
678
|
return !!process.stdout.isTTY && process.env["TERM"] !== "dumb" && process.env["CI"] !== "true" && process.env["ARCANE_DISABLE_ANIMATION"] !== "true" && (process.stdout.columns ?? 80) >= LOCKUP_WIDTH;
|
|
@@ -1795,8 +1795,9 @@ async function runAgentsList(targetDir) {
|
|
|
1795
1795
|
}
|
|
1796
1796
|
|
|
1797
1797
|
// src/modules/push-safety.ts
|
|
1798
|
-
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile5, chmod } from "fs/promises";
|
|
1799
|
-
import {
|
|
1798
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile5, chmod, access as access3, rm as rm2, readdir as readdir3 } from "fs/promises";
|
|
1799
|
+
import { constants } from "fs";
|
|
1800
|
+
import { join as join4, resolve, dirname, basename } from "path";
|
|
1800
1801
|
|
|
1801
1802
|
// src/modules/git.ts
|
|
1802
1803
|
import { execFile } from "child_process";
|
|
@@ -1893,7 +1894,7 @@ function buildGitEnv() {
|
|
|
1893
1894
|
function runGit(cwd, args, options = {}) {
|
|
1894
1895
|
const commandClass = options.commandClass ?? classifyGitCommand(args);
|
|
1895
1896
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUTS_MS[commandClass] ?? DEFAULT_TIMEOUTS_MS.write;
|
|
1896
|
-
return new Promise((
|
|
1897
|
+
return new Promise((resolve3, reject) => {
|
|
1897
1898
|
const child = execFile(
|
|
1898
1899
|
"git",
|
|
1899
1900
|
args,
|
|
@@ -1927,7 +1928,7 @@ function runGit(cwd, args, options = {}) {
|
|
|
1927
1928
|
reject(error);
|
|
1928
1929
|
return;
|
|
1929
1930
|
}
|
|
1930
|
-
|
|
1931
|
+
resolve3({ stdout, stderr });
|
|
1931
1932
|
}
|
|
1932
1933
|
);
|
|
1933
1934
|
child.stdin?.end();
|
|
@@ -2017,24 +2018,38 @@ async function ensureLocalPullRebase(cwd) {
|
|
|
2017
2018
|
}
|
|
2018
2019
|
|
|
2019
2020
|
// src/modules/push-safety.ts
|
|
2020
|
-
var DISABLED_PUSH_URL = "arcane-push-blocked
|
|
2021
|
+
var DISABLED_PUSH_URL = "arcane-push-blocked-run-spell-unblock-push://this-repository";
|
|
2022
|
+
var NO_PUSH_URL_MARKER = (remote) => `remote.${remote}.arcaneHadNoPushUrl`;
|
|
2021
2023
|
var ARCANE_HOOKS_DIR = ".arcane/hooks";
|
|
2022
2024
|
async function readHooksPath(cwd) {
|
|
2023
|
-
let
|
|
2025
|
+
let raw;
|
|
2024
2026
|
try {
|
|
2025
|
-
const { stdout } = await runGit(cwd, ["config", "--get", "core.hooksPath"]);
|
|
2026
|
-
|
|
2027
|
-
} catch {
|
|
2027
|
+
const { stdout } = await runGit(cwd, ["config", "--show-scope", "--get", "core.hooksPath"]);
|
|
2028
|
+
raw = stdout.trim();
|
|
2029
|
+
} catch (error) {
|
|
2030
|
+
if (error.code !== 1) {
|
|
2031
|
+
return { value: "", scope: "unknown", unreadable: true };
|
|
2032
|
+
}
|
|
2028
2033
|
return void 0;
|
|
2029
2034
|
}
|
|
2030
|
-
if (
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2035
|
+
if (raw === "") return void 0;
|
|
2036
|
+
const separator = raw.indexOf(" ");
|
|
2037
|
+
if (separator === -1) return { value: raw, scope: "unknown" };
|
|
2038
|
+
const scope = raw.slice(0, separator).trim();
|
|
2039
|
+
const value = raw.slice(separator + 1).trim();
|
|
2040
|
+
if (value === "") return void 0;
|
|
2041
|
+
const known = ["local", "worktree", "global", "system", "command"];
|
|
2042
|
+
const matched = known.find((candidate) => candidate === scope);
|
|
2043
|
+
return { value, scope: matched ?? "unknown" };
|
|
2044
|
+
}
|
|
2045
|
+
function isArcaneHooksPath(root, value) {
|
|
2046
|
+
const windows = process.platform === "win32";
|
|
2047
|
+
const normalize = (p) => {
|
|
2048
|
+
const separated = windows ? p.replace(/\\/g, "/") : p;
|
|
2049
|
+
const absolute = resolve(root, separated).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2050
|
+
return windows ? absolute.toLowerCase() : absolute;
|
|
2051
|
+
};
|
|
2052
|
+
return normalize(value) === normalize(ARCANE_HOOKS_DIR);
|
|
2038
2053
|
}
|
|
2039
2054
|
var HOOK_BODY = `#!/bin/sh
|
|
2040
2055
|
# Installed by Arcane because this repository's .arcane.json sets
|
|
@@ -2054,39 +2069,123 @@ var HOOK_BODY = `#!/bin/sh
|
|
|
2054
2069
|
echo "arcane: push blocked (push_policy: blocked). Run 'spell unblock-push' to undo." >&2
|
|
2055
2070
|
exit 1
|
|
2056
2071
|
`;
|
|
2057
|
-
function
|
|
2058
|
-
|
|
2072
|
+
function describeConfigScope(scope) {
|
|
2073
|
+
switch (scope) {
|
|
2074
|
+
case "local":
|
|
2075
|
+
return "set for this repository";
|
|
2076
|
+
case "worktree":
|
|
2077
|
+
return "set for this worktree";
|
|
2078
|
+
case "global":
|
|
2079
|
+
return "set globally, for your user";
|
|
2080
|
+
case "system":
|
|
2081
|
+
return "set system-wide";
|
|
2082
|
+
case "command":
|
|
2083
|
+
return "set on the command line";
|
|
2084
|
+
default:
|
|
2085
|
+
return "scope unknown";
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
async function repositoryRoot(cwd) {
|
|
2089
|
+
try {
|
|
2090
|
+
const { stdout } = await runGit(cwd, [
|
|
2091
|
+
"rev-parse",
|
|
2092
|
+
"--path-format=absolute",
|
|
2093
|
+
"--git-common-dir"
|
|
2094
|
+
]);
|
|
2095
|
+
const commonDir = stdout.trim();
|
|
2096
|
+
if (commonDir !== "") {
|
|
2097
|
+
const parent = dirname(commonDir);
|
|
2098
|
+
if (basename(commonDir) === ".git" && parent !== "") return parent;
|
|
2099
|
+
}
|
|
2100
|
+
} catch {
|
|
2101
|
+
}
|
|
2102
|
+
try {
|
|
2103
|
+
const { stdout } = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
2104
|
+
const top = stdout.trim();
|
|
2105
|
+
if (top !== "") return top;
|
|
2106
|
+
} catch {
|
|
2107
|
+
}
|
|
2108
|
+
return cwd;
|
|
2109
|
+
}
|
|
2110
|
+
async function hookFilePath(cwd) {
|
|
2111
|
+
return join4(await repositoryRoot(cwd), ARCANE_HOOKS_DIR, "pre-push");
|
|
2112
|
+
}
|
|
2113
|
+
async function hooksPathValue(cwd) {
|
|
2114
|
+
return join4(await repositoryRoot(cwd), ARCANE_HOOKS_DIR).replace(/\\/g, "/");
|
|
2059
2115
|
}
|
|
2060
2116
|
async function installPrePushHook(cwd) {
|
|
2117
|
+
const root = await repositoryRoot(cwd);
|
|
2061
2118
|
const existing = await readHooksPath(cwd);
|
|
2062
|
-
if (existing
|
|
2119
|
+
if (existing?.unreadable === true) return { status: "refused-unreadable-config" };
|
|
2120
|
+
if (existing !== void 0 && !isArcaneHooksPath(root, existing.value)) {
|
|
2063
2121
|
return {
|
|
2064
2122
|
status: "refused-foreign-hooks-path",
|
|
2065
2123
|
existing: existing.value,
|
|
2066
2124
|
scope: existing.scope
|
|
2067
2125
|
};
|
|
2068
2126
|
}
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2127
|
+
if (existing === void 0) {
|
|
2128
|
+
const displaced = await defaultHooks(cwd);
|
|
2129
|
+
if (displaced.length > 0) {
|
|
2130
|
+
return { status: "refused-default-hooks", hooks: displaced };
|
|
2131
|
+
}
|
|
2073
2132
|
}
|
|
2074
|
-
|
|
2133
|
+
const hookPath = join4(root, ARCANE_HOOKS_DIR, "pre-push");
|
|
2134
|
+
if (existing !== void 0 && await hookBodyMatches(cwd)) return { status: "already-ours" };
|
|
2135
|
+
await mkdir4(join4(root, ARCANE_HOOKS_DIR), { recursive: true });
|
|
2075
2136
|
await writeFile5(hookPath, HOOK_BODY, "utf-8");
|
|
2076
2137
|
await chmod(hookPath, 493);
|
|
2077
|
-
await runGit(cwd, ["config", "--local", "core.hooksPath",
|
|
2138
|
+
await runGit(cwd, ["config", "--local", "core.hooksPath", await hooksPathValue(cwd)]);
|
|
2078
2139
|
return { status: "installed", path: hookPath };
|
|
2079
2140
|
}
|
|
2080
2141
|
async function isHookEnforced(cwd) {
|
|
2081
2142
|
const hooksPath = await readHooksPath(cwd);
|
|
2082
|
-
if (hooksPath
|
|
2083
|
-
|
|
2143
|
+
if (hooksPath === void 0 || hooksPath.unreadable === true) return false;
|
|
2144
|
+
if (!isArcaneHooksPath(await repositoryRoot(cwd), hooksPath.value)) return false;
|
|
2145
|
+
return hookBodyMatches(cwd);
|
|
2146
|
+
}
|
|
2147
|
+
async function defaultHooks(cwd) {
|
|
2148
|
+
let hooksDir;
|
|
2149
|
+
try {
|
|
2150
|
+
const { stdout } = await runGit(cwd, ["rev-parse", "--git-common-dir"]);
|
|
2151
|
+
hooksDir = join4(resolve(cwd, stdout.trim()), "hooks");
|
|
2152
|
+
} catch {
|
|
2153
|
+
return [];
|
|
2154
|
+
}
|
|
2155
|
+
try {
|
|
2156
|
+
return (await readdir3(hooksDir)).filter((name) => !name.endsWith(".sample")).sort();
|
|
2157
|
+
} catch {
|
|
2158
|
+
return [];
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
async function hookBodyMatches(cwd) {
|
|
2162
|
+
const hookPath = await hookFilePath(cwd);
|
|
2163
|
+
if (!await fileExists(hookPath)) return false;
|
|
2164
|
+
try {
|
|
2165
|
+
if (await readFile4(hookPath, "utf-8") !== HOOK_BODY) return false;
|
|
2166
|
+
} catch {
|
|
2167
|
+
return false;
|
|
2168
|
+
}
|
|
2169
|
+
if (process.platform === "win32") return true;
|
|
2170
|
+
try {
|
|
2171
|
+
await access3(hookPath, constants.X_OK);
|
|
2172
|
+
return true;
|
|
2173
|
+
} catch {
|
|
2174
|
+
return false;
|
|
2175
|
+
}
|
|
2084
2176
|
}
|
|
2085
2177
|
async function removePrePushHook(cwd) {
|
|
2086
2178
|
const existing = await readHooksPath(cwd);
|
|
2087
|
-
if (existing
|
|
2179
|
+
if (existing === void 0 || existing.unreadable === true) return;
|
|
2180
|
+
if (!isArcaneHooksPath(await repositoryRoot(cwd), existing.value)) return;
|
|
2181
|
+
if (existing.scope !== "local" && existing.scope !== "worktree") return;
|
|
2182
|
+
const scopeFlag = existing.scope === "worktree" ? "--worktree" : "--local";
|
|
2183
|
+
try {
|
|
2184
|
+
await runGit(cwd, ["config", scopeFlag, "--unset", "core.hooksPath"]);
|
|
2185
|
+
} catch {
|
|
2186
|
+
}
|
|
2088
2187
|
try {
|
|
2089
|
-
await
|
|
2188
|
+
await rm2(await hookFilePath(cwd), { force: true });
|
|
2090
2189
|
} catch {
|
|
2091
2190
|
}
|
|
2092
2191
|
}
|
|
@@ -2098,62 +2197,203 @@ async function listRemotes(cwd) {
|
|
|
2098
2197
|
return [];
|
|
2099
2198
|
}
|
|
2100
2199
|
}
|
|
2101
|
-
async function
|
|
2102
|
-
const
|
|
2200
|
+
async function pushTargets(cwd) {
|
|
2201
|
+
const targets = [];
|
|
2103
2202
|
for (const remote of await listRemotes(cwd)) {
|
|
2104
|
-
let current;
|
|
2105
2203
|
try {
|
|
2106
|
-
const { stdout } = await runGit(cwd, ["remote", "get-url", "--push", remote]);
|
|
2107
|
-
|
|
2204
|
+
const { stdout } = await runGit(cwd, ["remote", "get-url", "--push", "--all", remote]);
|
|
2205
|
+
targets.push({
|
|
2206
|
+
remote,
|
|
2207
|
+
urls: stdout.split("\n").map((line) => line.trim()).filter(Boolean)
|
|
2208
|
+
});
|
|
2108
2209
|
} catch {
|
|
2109
|
-
|
|
2210
|
+
targets.push({ remote, urls: [] });
|
|
2110
2211
|
}
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2212
|
+
}
|
|
2213
|
+
return targets;
|
|
2214
|
+
}
|
|
2215
|
+
function originalPushUrlKey(remote) {
|
|
2216
|
+
return `remote.${remote}.arcaneOriginalPushUrl`;
|
|
2217
|
+
}
|
|
2218
|
+
async function readLocalAll(cwd, key) {
|
|
2219
|
+
try {
|
|
2220
|
+
const { stdout } = await runGit(cwd, ["config", "--local", "--get-all", key]);
|
|
2221
|
+
return stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
2222
|
+
} catch {
|
|
2223
|
+
return [];
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
async function readScopedAll(cwd, key) {
|
|
2227
|
+
let stdout;
|
|
2228
|
+
try {
|
|
2229
|
+
({ stdout } = await runGit(cwd, ["config", "--show-scope", "--get-all", key]));
|
|
2230
|
+
} catch {
|
|
2231
|
+
return [];
|
|
2232
|
+
}
|
|
2233
|
+
return stdout.split("\n").filter((line) => line !== "").map((line) => {
|
|
2234
|
+
const separator = line.indexOf(" ");
|
|
2235
|
+
if (separator === -1) return { scope: "unknown", value: line.trim() };
|
|
2236
|
+
return { scope: line.slice(0, separator).trim(), value: line.slice(separator + 1).trim() };
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
async function effectivePushUrls(cwd, remote) {
|
|
2240
|
+
const scoped = await readScopedAll(cwd, `remote.${remote}.pushurl`);
|
|
2241
|
+
return scoped.map((entry) => entry.value);
|
|
2242
|
+
}
|
|
2243
|
+
async function disablePushUrls(cwd) {
|
|
2244
|
+
const results = [];
|
|
2245
|
+
for (const remote of await listRemotes(cwd)) {
|
|
2246
|
+
try {
|
|
2247
|
+
results.push(await disableOneRemote(cwd, remote));
|
|
2248
|
+
} catch (error) {
|
|
2249
|
+
results.push({ remote, status: "failed", reason: describeError(error) });
|
|
2114
2250
|
}
|
|
2115
|
-
await runGit(cwd, ["config", "--local", `arcane.originalPushUrl.${remote}`, current]);
|
|
2116
|
-
await runGit(cwd, ["remote", "set-url", "--push", remote, DISABLED_PUSH_URL]);
|
|
2117
|
-
results.push({ remote, status: "disabled" });
|
|
2118
2251
|
}
|
|
2119
2252
|
return results;
|
|
2120
2253
|
}
|
|
2254
|
+
async function disableOneRemote(cwd, remote) {
|
|
2255
|
+
const scoped = await readScopedAll(cwd, `remote.${remote}.pushurl`);
|
|
2256
|
+
const OVERRIDABLE = /* @__PURE__ */ new Set(["local", "worktree"]);
|
|
2257
|
+
const foreign = scoped.filter(
|
|
2258
|
+
(entry) => !OVERRIDABLE.has(entry.scope) && entry.value !== DISABLED_PUSH_URL
|
|
2259
|
+
);
|
|
2260
|
+
if (foreign.length > 0) {
|
|
2261
|
+
const scopes = [...new Set(foreign.map((entry) => entry.scope))];
|
|
2262
|
+
const where = scopes.join(", ");
|
|
2263
|
+
const flag = scopes[0] === "unknown" ? "<scope>" : `--${scopes[0]}`;
|
|
2264
|
+
return {
|
|
2265
|
+
remote,
|
|
2266
|
+
status: "failed",
|
|
2267
|
+
reason: `a push URL for this remote is configured at ${where} scope, which this repository cannot override \u2014 git would still deliver the push there. Remove it with \`git config ${flag} --unset-all remote.${remote}.pushurl\` and re-apply.`
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
const currentPushUrls = scoped.map((entry) => entry.value);
|
|
2271
|
+
if (currentPushUrls.length === 1 && currentPushUrls[0] === DISABLED_PUSH_URL) {
|
|
2272
|
+
return { remote, status: "already-disabled" };
|
|
2273
|
+
}
|
|
2274
|
+
const key = originalPushUrlKey(remote);
|
|
2275
|
+
if ((await readLocalAll(cwd, key)).length === 0 && !await hadNoPushUrl(cwd, remote)) {
|
|
2276
|
+
if (currentPushUrls.length > 0) {
|
|
2277
|
+
await runGit(cwd, ["config", "--local", "--replace-all", key, currentPushUrls[0]]);
|
|
2278
|
+
for (const extra of currentPushUrls.slice(1)) {
|
|
2279
|
+
await runGit(cwd, ["config", "--local", "--add", key, extra]);
|
|
2280
|
+
}
|
|
2281
|
+
} else {
|
|
2282
|
+
await runGit(cwd, ["config", "--local", NO_PUSH_URL_MARKER(remote), "true"]);
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
await runGit(cwd, [
|
|
2286
|
+
"config",
|
|
2287
|
+
"--local",
|
|
2288
|
+
"--replace-all",
|
|
2289
|
+
`remote.${remote}.pushurl`,
|
|
2290
|
+
DISABLED_PUSH_URL
|
|
2291
|
+
]);
|
|
2292
|
+
const after = await effectivePushUrls(cwd, remote);
|
|
2293
|
+
if (after.length !== 1 || after[0] !== DISABLED_PUSH_URL) {
|
|
2294
|
+
const live = after.filter((url) => url !== DISABLED_PUSH_URL);
|
|
2295
|
+
return {
|
|
2296
|
+
remote,
|
|
2297
|
+
status: "failed",
|
|
2298
|
+
reason: `the sentinel was written but git still resolves a live push URL for this remote (${live.join(", ")}). It most likely comes from a file this repository \`include\`s, which \`--replace-all\` cannot remove. Run \`git config --show-origin --get-all remote.${remote}.pushurl\` to find the file, and clear it there.`
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
return { remote, status: "disabled" };
|
|
2302
|
+
}
|
|
2121
2303
|
async function undisabledRemotes(cwd) {
|
|
2122
2304
|
const open = [];
|
|
2123
2305
|
for (const remote of await listRemotes(cwd)) {
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
if (stdout.trim() !== DISABLED_PUSH_URL) open.push(remote);
|
|
2127
|
-
} catch {
|
|
2128
|
-
open.push(remote);
|
|
2129
|
-
}
|
|
2306
|
+
const urls = await effectivePushUrls(cwd, remote);
|
|
2307
|
+
if (urls.length === 0 || urls.some((url) => url !== DISABLED_PUSH_URL)) open.push(remote);
|
|
2130
2308
|
}
|
|
2131
2309
|
return open;
|
|
2132
2310
|
}
|
|
2311
|
+
async function blockedRemotes(cwd) {
|
|
2312
|
+
const blocked = [];
|
|
2313
|
+
for (const remote of await listRemotes(cwd)) {
|
|
2314
|
+
const urls = await effectivePushUrls(cwd, remote);
|
|
2315
|
+
if (urls.some((url) => url === DISABLED_PUSH_URL)) blocked.push(remote);
|
|
2316
|
+
}
|
|
2317
|
+
return blocked;
|
|
2318
|
+
}
|
|
2133
2319
|
async function restorePushUrls(cwd) {
|
|
2134
|
-
const
|
|
2320
|
+
const results = [];
|
|
2135
2321
|
for (const remote of await listRemotes(cwd)) {
|
|
2136
|
-
let original = "";
|
|
2137
2322
|
try {
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2323
|
+
results.push(await restoreOneRemote(cwd, remote));
|
|
2324
|
+
} catch (error) {
|
|
2325
|
+
results.push({ remote, status: "failed", reason: describeError(error) });
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
return results;
|
|
2329
|
+
}
|
|
2330
|
+
async function restoreOneRemote(cwd, remote) {
|
|
2331
|
+
const key = originalPushUrlKey(remote);
|
|
2332
|
+
let recorded = await readLocalAll(cwd, key);
|
|
2333
|
+
let hadNone = await hadNoPushUrl(cwd, remote);
|
|
2334
|
+
if (recorded.length === 0 && !hadNone) {
|
|
2335
|
+
const legacy = await readLegacyRecord(cwd, remote);
|
|
2336
|
+
if (legacy.status !== "usable") return { remote, status: legacy.status, reason: legacy.reason };
|
|
2337
|
+
recorded = legacy.values;
|
|
2338
|
+
hadNone = false;
|
|
2339
|
+
}
|
|
2340
|
+
if (hadNone) {
|
|
2341
|
+
await unsetIgnoringMissing(cwd, `remote.${remote}.pushurl`);
|
|
2342
|
+
} else {
|
|
2343
|
+
await runGit(cwd, [
|
|
2344
|
+
"config",
|
|
2345
|
+
"--local",
|
|
2346
|
+
"--replace-all",
|
|
2347
|
+
`remote.${remote}.pushurl`,
|
|
2348
|
+
recorded[0]
|
|
2349
|
+
]);
|
|
2350
|
+
for (const extra of recorded.slice(1)) {
|
|
2351
|
+
await runGit(cwd, ["config", "--local", "--add", `remote.${remote}.pushurl`, extra]);
|
|
2147
2352
|
}
|
|
2148
|
-
|
|
2149
|
-
|
|
2353
|
+
}
|
|
2354
|
+
for (const staleKey of [key, NO_PUSH_URL_MARKER(remote), `arcane.originalPushUrl.${remote}`]) {
|
|
2150
2355
|
try {
|
|
2151
|
-
await
|
|
2356
|
+
await unsetIgnoringMissing(cwd, staleKey);
|
|
2152
2357
|
} catch {
|
|
2153
2358
|
}
|
|
2154
|
-
restored.push(remote);
|
|
2155
2359
|
}
|
|
2156
|
-
return restored;
|
|
2360
|
+
return { remote, status: "restored" };
|
|
2361
|
+
}
|
|
2362
|
+
async function unsetIgnoringMissing(cwd, key) {
|
|
2363
|
+
try {
|
|
2364
|
+
await runGit(cwd, ["config", "--local", "--unset-all", key]);
|
|
2365
|
+
} catch (error) {
|
|
2366
|
+
if (error.code !== 5) throw error;
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
async function hadNoPushUrl(cwd, remote) {
|
|
2370
|
+
return (await readLocalAll(cwd, NO_PUSH_URL_MARKER(remote)))[0] === "true";
|
|
2371
|
+
}
|
|
2372
|
+
async function readLegacyRecord(cwd, remote) {
|
|
2373
|
+
const values = await readLocalAll(cwd, `arcane.originalPushUrl.${remote}`);
|
|
2374
|
+
if (values.length === 0) return { status: "nothing-recorded" };
|
|
2375
|
+
const current = await effectivePushUrls(cwd, remote);
|
|
2376
|
+
if (!current.includes(DISABLED_PUSH_URL)) {
|
|
2377
|
+
return {
|
|
2378
|
+
status: "failed",
|
|
2379
|
+
reason: `found a push URL recorded by 0.20.0 ("${values.join(", ")}") but this remote is not currently blocked by Arcane, so the record may belong to a different remote of the same name. Not applying it \u2014 set the URL yourself if it is correct.`
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
const names = await listRemotes(cwd);
|
|
2383
|
+
const collides = names.some(
|
|
2384
|
+
(other) => other !== remote && other.toLowerCase() === remote.toLowerCase()
|
|
2385
|
+
);
|
|
2386
|
+
if (collides) {
|
|
2387
|
+
return {
|
|
2388
|
+
status: "failed",
|
|
2389
|
+
reason: `this repository has remotes whose names differ only by case, and 0.20.0 stored their original push URLs under one shared key, so the recorded value ("${values.join(", ")}") cannot be attributed to a specific remote. Not applying it \u2014 set the URLs yourself.`
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
return { status: "usable", values };
|
|
2393
|
+
}
|
|
2394
|
+
function describeError(error) {
|
|
2395
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2396
|
+
return message.split("\n")[0].trim();
|
|
2157
2397
|
}
|
|
2158
2398
|
|
|
2159
2399
|
// src/commands/init.ts
|
|
@@ -2483,22 +2723,37 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
2483
2723
|
try {
|
|
2484
2724
|
if (push_policy === "blocked") {
|
|
2485
2725
|
const hook = await installPrePushHook(targetDir);
|
|
2486
|
-
if (hook.status === "refused-
|
|
2726
|
+
if (hook.status === "refused-unreadable-config") {
|
|
2487
2727
|
printWarning(
|
|
2488
|
-
|
|
2728
|
+
"Did not install the pre-push hook: git could not report the current core.hooksPath (unreadable config, or a git too old for `--show-scope`). Installing without knowing whether another hook manager owns that setting could silently disable it. Fix the config and run `spell doctor`."
|
|
2729
|
+
);
|
|
2730
|
+
} else if (hook.status === "refused-default-hooks") {
|
|
2731
|
+
printWarning(
|
|
2732
|
+
`Did not install the pre-push hook: this repository has hooks in git's default directory (${hook.hooks.join(", ")}). Setting core.hooksPath would silently stop them running. Move them under \`${ARCANE_HOOKS_DIR}\` yourself and re-run, or add the push guard to your existing pre-push hook.`
|
|
2733
|
+
);
|
|
2734
|
+
} else if (hook.status === "refused-foreign-hooks-path") {
|
|
2735
|
+
printWarning(
|
|
2736
|
+
`Did not install the pre-push hook: core.hooksPath is already "${hook.existing}" (${describeConfigScope(hook.scope)}), so another hook manager owns it. Overwriting would silently disable those hooks. Chain an Arcane pre-push guard into that directory yourself, or unset core.hooksPath first.`
|
|
2489
2737
|
);
|
|
2490
2738
|
} else {
|
|
2491
2739
|
printInfo("Installed a pre-push hook that blocks pushes from this repository.");
|
|
2492
2740
|
}
|
|
2493
2741
|
const urls = await disablePushUrls(targetDir);
|
|
2742
|
+
const unprotected = urls.filter((u) => u.status === "failed");
|
|
2743
|
+
if (unprotected.length > 0) {
|
|
2744
|
+
printWarning(
|
|
2745
|
+
`Could not disable the push URL for: ${unprotected.map((u) => `${u.remote} (${u.reason ?? "unknown error"})`).join("; ")}. Those remotes are still pushable with \`--no-verify\`. Run \`spell doctor\` \u2014 it lists exactly which remotes are still live.`
|
|
2746
|
+
);
|
|
2747
|
+
}
|
|
2494
2748
|
if (urls.length === 0) {
|
|
2495
2749
|
printWarning(
|
|
2496
2750
|
"No remote is configured, so only the pre-push hook is active. A remote added later will NOT have its push URL disabled automatically, and a `--no-verify` push to it would succeed. Run `spell doctor` after adding one \u2014 it reports this gap."
|
|
2497
2751
|
);
|
|
2498
2752
|
} else {
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2753
|
+
const covered = urls.filter((u) => u.status !== "failed").map((u) => u.remote);
|
|
2754
|
+
if (covered.length > 0) {
|
|
2755
|
+
printInfo(`Disabled the push URL for: ${covered.join(", ")} (fetch still works).`);
|
|
2756
|
+
}
|
|
2502
2757
|
}
|
|
2503
2758
|
printInfo("Run `spell unblock-push` from a terminal to undo this.");
|
|
2504
2759
|
} else {
|
|
@@ -2636,7 +2891,7 @@ import { join as join8 } from "path";
|
|
|
2636
2891
|
|
|
2637
2892
|
// src/modules/hub.ts
|
|
2638
2893
|
import { confirm as confirm2, select as select3, input as input3 } from "@inquirer/prompts";
|
|
2639
|
-
import { readdir as
|
|
2894
|
+
import { readdir as readdir4, writeFile as writeFile6 } from "fs/promises";
|
|
2640
2895
|
import { join as join7 } from "path";
|
|
2641
2896
|
var MANIFEST_RETROFITS = [
|
|
2642
2897
|
{
|
|
@@ -2780,7 +3035,7 @@ async function offerRegistryScaffold(targetDir, businessRoot) {
|
|
|
2780
3035
|
const businessRootPath = join7(targetDir, businessRoot);
|
|
2781
3036
|
let ventureDirs;
|
|
2782
3037
|
try {
|
|
2783
|
-
const entries = await
|
|
3038
|
+
const entries = await readdir4(businessRootPath, { withFileTypes: true });
|
|
2784
3039
|
ventureDirs = entries.filter((e) => e.isDirectory() && e.name !== "_template").map((e) => e.name);
|
|
2785
3040
|
} catch {
|
|
2786
3041
|
return;
|
|
@@ -2987,7 +3242,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
2987
3242
|
|
|
2988
3243
|
// src/modules/version-check.ts
|
|
2989
3244
|
import { readFile as readFile5, writeFile as writeFile7, mkdir as mkdir5 } from "fs/promises";
|
|
2990
|
-
import { dirname, join as join9 } from "path";
|
|
3245
|
+
import { dirname as dirname2, join as join9 } from "path";
|
|
2991
3246
|
import { homedir as homedir2 } from "os";
|
|
2992
3247
|
import { createRequire } from "module";
|
|
2993
3248
|
var require2 = createRequire(import.meta.url);
|
|
@@ -3016,7 +3271,7 @@ async function readCache(cachePath) {
|
|
|
3016
3271
|
}
|
|
3017
3272
|
}
|
|
3018
3273
|
async function writeCache(cachePath, latest) {
|
|
3019
|
-
await mkdir5(
|
|
3274
|
+
await mkdir5(dirname2(cachePath), { recursive: true });
|
|
3020
3275
|
const entry = { latest, checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3021
3276
|
await writeFile7(cachePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
3022
3277
|
}
|
|
@@ -3130,7 +3385,7 @@ async function runStatus(targetDir, packageVersion) {
|
|
|
3130
3385
|
}
|
|
3131
3386
|
|
|
3132
3387
|
// src/commands/uninstall.ts
|
|
3133
|
-
import { rm as
|
|
3388
|
+
import { rm as rm3 } from "fs/promises";
|
|
3134
3389
|
import { join as join10 } from "path";
|
|
3135
3390
|
import { confirm as confirm3 } from "@inquirer/prompts";
|
|
3136
3391
|
async function runUninstall(options, targetDir) {
|
|
@@ -3145,6 +3400,21 @@ async function runUninstall(options, targetDir) {
|
|
|
3145
3400
|
}
|
|
3146
3401
|
throw err;
|
|
3147
3402
|
}
|
|
3403
|
+
const policy = manifest.push_policy ?? "open";
|
|
3404
|
+
const controlsInForce = await isHookEnforced(targetDir) || (await blockedRemotes(targetDir)).length > 0;
|
|
3405
|
+
if ((policy === "blocked" || controlsInForce) && !options.dryRun) {
|
|
3406
|
+
const why = policy === "blocked" ? `this repository's push_policy is "blocked"` : `this repository still has Arcane's push block in force (the manifest says "${policy}", but the controls are actually installed)`;
|
|
3407
|
+
console.error(
|
|
3408
|
+
`Refusing to uninstall: ${why}.
|
|
3409
|
+
Uninstalling now would delete .arcane.json while leaving the pre-push hook and the
|
|
3410
|
+
disabled push URLs in place \u2014 the repository could not push, and \`spell unblock-push\`
|
|
3411
|
+
would no longer recognise it.
|
|
3412
|
+
|
|
3413
|
+
Run \`spell unblock-push\` first (interactive), then uninstall.`
|
|
3414
|
+
);
|
|
3415
|
+
process.exit(1);
|
|
3416
|
+
return;
|
|
3417
|
+
}
|
|
3148
3418
|
const allFiles = manifest.components.flatMap((c) => c.files);
|
|
3149
3419
|
const totalFiles = allFiles.length;
|
|
3150
3420
|
if (options.dryRun) {
|
|
@@ -3159,6 +3429,11 @@ async function runUninstall(options, targetDir) {
|
|
|
3159
3429
|
console.log(`[dry-run] Would strip arcane sections from: CLAUDE.md, AGENTS.md, copilot-instructions.md`);
|
|
3160
3430
|
console.log(`
|
|
3161
3431
|
[dry-run] ${totalFiles} file(s) + manifest + agent outputs would be removed.`);
|
|
3432
|
+
if (policy === "blocked" || controlsInForce) {
|
|
3433
|
+
console.log(
|
|
3434
|
+
`[dry-run] BUT the real run would refuse: Arcane's push block is in force. Run \`spell unblock-push\` first.`
|
|
3435
|
+
);
|
|
3436
|
+
}
|
|
3162
3437
|
return;
|
|
3163
3438
|
}
|
|
3164
3439
|
if (!options.yes) {
|
|
@@ -3176,22 +3451,22 @@ async function runUninstall(options, targetDir) {
|
|
|
3176
3451
|
for (const file of component.files) {
|
|
3177
3452
|
const filePath = join10(targetDir, file);
|
|
3178
3453
|
try {
|
|
3179
|
-
await
|
|
3454
|
+
await rm3(filePath, { force: true });
|
|
3180
3455
|
removed++;
|
|
3181
3456
|
} catch {
|
|
3182
3457
|
}
|
|
3183
3458
|
}
|
|
3184
3459
|
}
|
|
3185
|
-
await
|
|
3460
|
+
await rm3(join10(targetDir, ".arcane.json"), { force: true });
|
|
3186
3461
|
const agentDirs = [
|
|
3187
3462
|
".github/agents",
|
|
3188
3463
|
".arcane/agents",
|
|
3189
3464
|
".arcane/generated"
|
|
3190
3465
|
];
|
|
3191
3466
|
for (const dir of agentDirs) {
|
|
3192
|
-
await
|
|
3467
|
+
await rm3(join10(targetDir, dir), { recursive: true, force: true });
|
|
3193
3468
|
}
|
|
3194
|
-
await
|
|
3469
|
+
await rm3(join10(targetDir, ".arcane/agents.yaml"), { force: true });
|
|
3195
3470
|
const markerFiles = [
|
|
3196
3471
|
"CLAUDE.md",
|
|
3197
3472
|
"AGENTS.md",
|
|
@@ -3205,8 +3480,8 @@ async function runUninstall(options, targetDir) {
|
|
|
3205
3480
|
}
|
|
3206
3481
|
|
|
3207
3482
|
// src/commands/doctor.ts
|
|
3208
|
-
import { access as
|
|
3209
|
-
import { join as join11, dirname as
|
|
3483
|
+
import { access as access4, readFile as readFile6, mkdir as mkdir6, copyFile as fsCopyFile2 } from "fs/promises";
|
|
3484
|
+
import { join as join11, dirname as dirname3 } from "path";
|
|
3210
3485
|
import { execFile as execFile2 } from "child_process";
|
|
3211
3486
|
import { promisify } from "util";
|
|
3212
3487
|
|
|
@@ -3299,8 +3574,8 @@ async function checkVSCodeExtension(id, label) {
|
|
|
3299
3574
|
];
|
|
3300
3575
|
for (const dir of extDirs) {
|
|
3301
3576
|
try {
|
|
3302
|
-
const { readdir:
|
|
3303
|
-
const entries = await
|
|
3577
|
+
const { readdir: readdir5 } = await import("fs/promises");
|
|
3578
|
+
const entries = await readdir5(dir);
|
|
3304
3579
|
const prefix = id.toLowerCase() + "-";
|
|
3305
3580
|
const found = entries.some(
|
|
3306
3581
|
(e) => e.toLowerCase() === id.toLowerCase() || e.toLowerCase().startsWith(prefix)
|
|
@@ -3331,7 +3606,7 @@ async function checkArcaneManifest(targetDir) {
|
|
|
3331
3606
|
const name = "Arcane manifest (.arcane.json)";
|
|
3332
3607
|
const manifestPath2 = join11(targetDir, ".arcane.json");
|
|
3333
3608
|
try {
|
|
3334
|
-
await
|
|
3609
|
+
await access4(manifestPath2);
|
|
3335
3610
|
} catch {
|
|
3336
3611
|
const selfHostedManifestPath = join11(targetDir, "src", "assets", ".arcane.json");
|
|
3337
3612
|
try {
|
|
@@ -3427,7 +3702,7 @@ async function checkSessionContinuity(targetDir) {
|
|
|
3427
3702
|
const name = `Session file: ${file}`;
|
|
3428
3703
|
const filePath = join11(targetDir, file);
|
|
3429
3704
|
try {
|
|
3430
|
-
await
|
|
3705
|
+
await access4(filePath);
|
|
3431
3706
|
results.push({ name, passed: true, message: "present" });
|
|
3432
3707
|
} catch {
|
|
3433
3708
|
results.push({
|
|
@@ -3445,10 +3720,10 @@ async function fixSessionContinuity(targetDir, assetsDir) {
|
|
|
3445
3720
|
for (const file of SESSION_CONTINUITY_FILES) {
|
|
3446
3721
|
const destPath = join11(targetDir, file);
|
|
3447
3722
|
try {
|
|
3448
|
-
await
|
|
3723
|
+
await access4(destPath);
|
|
3449
3724
|
} catch {
|
|
3450
3725
|
const srcPath = join11(assetsDir, file);
|
|
3451
|
-
await mkdir6(
|
|
3726
|
+
await mkdir6(dirname3(destPath), { recursive: true });
|
|
3452
3727
|
await fsCopyFile2(srcPath, destPath);
|
|
3453
3728
|
created.push(file);
|
|
3454
3729
|
}
|
|
@@ -3468,8 +3743,8 @@ async function checkPushPolicy(targetDir) {
|
|
|
3468
3743
|
return { name, passed: true, message: "open \u2014 pushes allowed" };
|
|
3469
3744
|
}
|
|
3470
3745
|
if (policy === "guarded") {
|
|
3471
|
-
const
|
|
3472
|
-
const where =
|
|
3746
|
+
const targets = await pushTargets(targetDir);
|
|
3747
|
+
const where = targets.length === 0 ? "none configured" : targets.map(({ remote, urls }) => `${remote} \u2192 ${urls.length > 0 ? urls.join(", ") : "unknown"}`).join("; ");
|
|
3473
3748
|
return {
|
|
3474
3749
|
name,
|
|
3475
3750
|
passed: false,
|
|
@@ -3559,7 +3834,7 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
|
3559
3834
|
}
|
|
3560
3835
|
|
|
3561
3836
|
// src/commands/unblock-push.ts
|
|
3562
|
-
import { basename, resolve } from "path";
|
|
3837
|
+
import { basename as basename2, resolve as resolve2 } from "path";
|
|
3563
3838
|
import { input as input4 } from "@inquirer/prompts";
|
|
3564
3839
|
async function runUnblockPush(targetDir) {
|
|
3565
3840
|
let manifest;
|
|
@@ -3574,10 +3849,16 @@ async function runUnblockPush(targetDir) {
|
|
|
3574
3849
|
throw err;
|
|
3575
3850
|
}
|
|
3576
3851
|
const policy = manifest.push_policy ?? "open";
|
|
3577
|
-
|
|
3852
|
+
const stillInForce = await isHookEnforced(targetDir) || (await blockedRemotes(targetDir)).length > 0;
|
|
3853
|
+
if (policy === "open" && !stillInForce) {
|
|
3578
3854
|
printInfo("This repository is not push-blocked \u2014 nothing to undo.");
|
|
3579
3855
|
return;
|
|
3580
3856
|
}
|
|
3857
|
+
if (policy === "open") {
|
|
3858
|
+
printWarning(
|
|
3859
|
+
"The manifest says push_policy: open, but this repository still has Arcane's push block in force \u2014 most likely a previous unblock only partly succeeded. Continuing so it can be finished."
|
|
3860
|
+
);
|
|
3861
|
+
}
|
|
3581
3862
|
if (!process.stdin.isTTY) {
|
|
3582
3863
|
console.error(
|
|
3583
3864
|
"spell unblock-push must be run from an interactive terminal.\nLifting a push block is a deliberate decision, so it cannot be scripted, piped, or run in CI."
|
|
@@ -3585,7 +3866,7 @@ async function runUnblockPush(targetDir) {
|
|
|
3585
3866
|
process.exit(1);
|
|
3586
3867
|
return;
|
|
3587
3868
|
}
|
|
3588
|
-
const repoName =
|
|
3869
|
+
const repoName = basename2(resolve2(targetDir));
|
|
3589
3870
|
if (repoName === "") {
|
|
3590
3871
|
console.error(
|
|
3591
3872
|
"Could not determine a repository name to confirm against (is this repository at a filesystem root?). Refusing to unblock."
|
|
@@ -3608,17 +3889,43 @@ Name did not match ("${typed.trim()}" \u2260 "${repoName}"). Nothing changed.`);
|
|
|
3608
3889
|
return;
|
|
3609
3890
|
}
|
|
3610
3891
|
await removePrePushHook(targetDir);
|
|
3611
|
-
const
|
|
3892
|
+
const results = await restorePushUrls(targetDir);
|
|
3893
|
+
const restored = results.filter((r) => r.status === "restored").map((r) => r.remote);
|
|
3894
|
+
const failed = results.filter((r) => r.status === "failed");
|
|
3895
|
+
const stillBlocked = await blockedRemotes(targetDir);
|
|
3612
3896
|
const updated = {
|
|
3613
3897
|
...manifest,
|
|
3614
3898
|
push_policy: "open",
|
|
3615
3899
|
push_policy_unblocked_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3616
3900
|
};
|
|
3617
3901
|
await writeManifest(targetDir, updated);
|
|
3618
|
-
|
|
3902
|
+
const hookStillOn = await isHookEnforced(targetDir);
|
|
3903
|
+
const fullyLifted = failed.length === 0 && stillBlocked.length === 0 && !hookStillOn;
|
|
3904
|
+
if (fullyLifted) {
|
|
3905
|
+
printSuccess(`Push unblocked for "${repoName}".`);
|
|
3906
|
+
} else {
|
|
3907
|
+
printWarning(
|
|
3908
|
+
`Push only PARTIALLY unblocked for "${repoName}". The manifest now says open; the repository is not fully open yet. Details below.`
|
|
3909
|
+
);
|
|
3910
|
+
}
|
|
3619
3911
|
if (restored.length > 0) {
|
|
3620
3912
|
printInfo(`Restored push URL for: ${restored.join(", ")}.`);
|
|
3621
3913
|
}
|
|
3914
|
+
if (hookStillOn) {
|
|
3915
|
+
printWarning(
|
|
3916
|
+
"The pre-push hook is still in force. Arcane did not remove it because `core.hooksPath` is not one it owns, or could not be read. Check `git config --show-scope --get core.hooksPath`."
|
|
3917
|
+
);
|
|
3918
|
+
}
|
|
3919
|
+
if (failed.length > 0) {
|
|
3920
|
+
printWarning(
|
|
3921
|
+
`Could not restore the push URL for: ${failed.map((r) => `${r.remote} (${r.reason ?? "unknown error"})`).join("; ")}.`
|
|
3922
|
+
);
|
|
3923
|
+
}
|
|
3924
|
+
if (stillBlocked.length > 0) {
|
|
3925
|
+
printWarning(
|
|
3926
|
+
`These remotes still have a blocked push URL and no recorded original: ${stillBlocked.join(", ")}. Set them yourself with \`git remote set-url --push <name> <url>\` \u2014 until then, pushing to them will fail even though the manifest now says open.`
|
|
3927
|
+
);
|
|
3928
|
+
}
|
|
3622
3929
|
printInfo(
|
|
3623
3930
|
"Recorded in .arcane.json as push_policy: open, with the time it was lifted. Commit that change so the repository's actual posture stays visible to everyone."
|
|
3624
3931
|
);
|
|
@@ -3628,7 +3935,7 @@ Name did not match ("${typed.trim()}" \u2260 "${repoName}"). Nothing changed.`);
|
|
|
3628
3935
|
import { realpathSync } from "fs";
|
|
3629
3936
|
var require3 = createRequire2(import.meta.url);
|
|
3630
3937
|
var pkg = require3("../package.json");
|
|
3631
|
-
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join12(
|
|
3938
|
+
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join12(dirname4(fileURLToPath(import.meta.url)), "assets");
|
|
3632
3939
|
var program = new Command();
|
|
3633
3940
|
program.name("spell").description("Arcane framework CLI \u2014 scaffold and manage governance files").version(pkg.version).action(async () => {
|
|
3634
3941
|
await printWelcome(pkg.version, process.cwd());
|