arcane-cli 0.20.1 → 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 +112 -34
- 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";
|
|
@@ -1795,9 +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, access as access3, rm as rm2 } from "fs/promises";
|
|
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
1799
|
import { constants } from "fs";
|
|
1800
|
-
import { join as join4, resolve } from "path";
|
|
1800
|
+
import { join as join4, resolve, dirname, basename } from "path";
|
|
1801
1801
|
|
|
1802
1802
|
// src/modules/git.ts
|
|
1803
1803
|
import { execFile } from "child_process";
|
|
@@ -2042,11 +2042,11 @@ async function readHooksPath(cwd) {
|
|
|
2042
2042
|
const matched = known.find((candidate) => candidate === scope);
|
|
2043
2043
|
return { value, scope: matched ?? "unknown" };
|
|
2044
2044
|
}
|
|
2045
|
-
function isArcaneHooksPath(
|
|
2045
|
+
function isArcaneHooksPath(root, value) {
|
|
2046
2046
|
const windows = process.platform === "win32";
|
|
2047
2047
|
const normalize = (p) => {
|
|
2048
2048
|
const separated = windows ? p.replace(/\\/g, "/") : p;
|
|
2049
|
-
const absolute = resolve(
|
|
2049
|
+
const absolute = resolve(root, separated).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2050
2050
|
return windows ? absolute.toLowerCase() : absolute;
|
|
2051
2051
|
};
|
|
2052
2052
|
return normalize(value) === normalize(ARCANE_HOOKS_DIR);
|
|
@@ -2085,34 +2085,81 @@ function describeConfigScope(scope) {
|
|
|
2085
2085
|
return "scope unknown";
|
|
2086
2086
|
}
|
|
2087
2087
|
}
|
|
2088
|
-
function
|
|
2089
|
-
|
|
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, "/");
|
|
2090
2115
|
}
|
|
2091
2116
|
async function installPrePushHook(cwd) {
|
|
2117
|
+
const root = await repositoryRoot(cwd);
|
|
2092
2118
|
const existing = await readHooksPath(cwd);
|
|
2093
2119
|
if (existing?.unreadable === true) return { status: "refused-unreadable-config" };
|
|
2094
|
-
if (existing !== void 0 && !isArcaneHooksPath(
|
|
2120
|
+
if (existing !== void 0 && !isArcaneHooksPath(root, existing.value)) {
|
|
2095
2121
|
return {
|
|
2096
2122
|
status: "refused-foreign-hooks-path",
|
|
2097
2123
|
existing: existing.value,
|
|
2098
2124
|
scope: existing.scope
|
|
2099
2125
|
};
|
|
2100
2126
|
}
|
|
2101
|
-
|
|
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
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
const hookPath = join4(root, ARCANE_HOOKS_DIR, "pre-push");
|
|
2102
2134
|
if (existing !== void 0 && await hookBodyMatches(cwd)) return { status: "already-ours" };
|
|
2103
|
-
await mkdir4(join4(
|
|
2135
|
+
await mkdir4(join4(root, ARCANE_HOOKS_DIR), { recursive: true });
|
|
2104
2136
|
await writeFile5(hookPath, HOOK_BODY, "utf-8");
|
|
2105
2137
|
await chmod(hookPath, 493);
|
|
2106
|
-
await runGit(cwd, ["config", "--local", "core.hooksPath",
|
|
2138
|
+
await runGit(cwd, ["config", "--local", "core.hooksPath", await hooksPathValue(cwd)]);
|
|
2107
2139
|
return { status: "installed", path: hookPath };
|
|
2108
2140
|
}
|
|
2109
2141
|
async function isHookEnforced(cwd) {
|
|
2110
2142
|
const hooksPath = await readHooksPath(cwd);
|
|
2111
|
-
if (hooksPath === void 0 ||
|
|
2143
|
+
if (hooksPath === void 0 || hooksPath.unreadable === true) return false;
|
|
2144
|
+
if (!isArcaneHooksPath(await repositoryRoot(cwd), hooksPath.value)) return false;
|
|
2112
2145
|
return hookBodyMatches(cwd);
|
|
2113
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
|
+
}
|
|
2114
2161
|
async function hookBodyMatches(cwd) {
|
|
2115
|
-
const hookPath = hookFilePath(cwd);
|
|
2162
|
+
const hookPath = await hookFilePath(cwd);
|
|
2116
2163
|
if (!await fileExists(hookPath)) return false;
|
|
2117
2164
|
try {
|
|
2118
2165
|
if (await readFile4(hookPath, "utf-8") !== HOOK_BODY) return false;
|
|
@@ -2130,7 +2177,7 @@ async function hookBodyMatches(cwd) {
|
|
|
2130
2177
|
async function removePrePushHook(cwd) {
|
|
2131
2178
|
const existing = await readHooksPath(cwd);
|
|
2132
2179
|
if (existing === void 0 || existing.unreadable === true) return;
|
|
2133
|
-
if (!isArcaneHooksPath(cwd, existing.value)) return;
|
|
2180
|
+
if (!isArcaneHooksPath(await repositoryRoot(cwd), existing.value)) return;
|
|
2134
2181
|
if (existing.scope !== "local" && existing.scope !== "worktree") return;
|
|
2135
2182
|
const scopeFlag = existing.scope === "worktree" ? "--worktree" : "--local";
|
|
2136
2183
|
try {
|
|
@@ -2138,7 +2185,7 @@ async function removePrePushHook(cwd) {
|
|
|
2138
2185
|
} catch {
|
|
2139
2186
|
}
|
|
2140
2187
|
try {
|
|
2141
|
-
await rm2(hookFilePath(cwd), { force: true });
|
|
2188
|
+
await rm2(await hookFilePath(cwd), { force: true });
|
|
2142
2189
|
} catch {
|
|
2143
2190
|
}
|
|
2144
2191
|
}
|
|
@@ -2183,11 +2230,11 @@ async function readScopedAll(cwd, key) {
|
|
|
2183
2230
|
} catch {
|
|
2184
2231
|
return [];
|
|
2185
2232
|
}
|
|
2186
|
-
return stdout.split("\n").
|
|
2233
|
+
return stdout.split("\n").filter((line) => line !== "").map((line) => {
|
|
2187
2234
|
const separator = line.indexOf(" ");
|
|
2188
2235
|
if (separator === -1) return { scope: "unknown", value: line.trim() };
|
|
2189
2236
|
return { scope: line.slice(0, separator).trim(), value: line.slice(separator + 1).trim() };
|
|
2190
|
-
})
|
|
2237
|
+
});
|
|
2191
2238
|
}
|
|
2192
2239
|
async function effectivePushUrls(cwd, remote) {
|
|
2193
2240
|
const scoped = await readScopedAll(cwd, `remote.${remote}.pushurl`);
|
|
@@ -2206,13 +2253,18 @@ async function disablePushUrls(cwd) {
|
|
|
2206
2253
|
}
|
|
2207
2254
|
async function disableOneRemote(cwd, remote) {
|
|
2208
2255
|
const scoped = await readScopedAll(cwd, `remote.${remote}.pushurl`);
|
|
2209
|
-
const
|
|
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
|
+
);
|
|
2210
2260
|
if (foreign.length > 0) {
|
|
2211
|
-
const
|
|
2261
|
+
const scopes = [...new Set(foreign.map((entry) => entry.scope))];
|
|
2262
|
+
const where = scopes.join(", ");
|
|
2263
|
+
const flag = scopes[0] === "unknown" ? "<scope>" : `--${scopes[0]}`;
|
|
2212
2264
|
return {
|
|
2213
2265
|
remote,
|
|
2214
2266
|
status: "failed",
|
|
2215
|
-
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
|
|
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.`
|
|
2216
2268
|
};
|
|
2217
2269
|
}
|
|
2218
2270
|
const currentPushUrls = scoped.map((entry) => entry.value);
|
|
@@ -2237,6 +2289,15 @@ async function disableOneRemote(cwd, remote) {
|
|
|
2237
2289
|
`remote.${remote}.pushurl`,
|
|
2238
2290
|
DISABLED_PUSH_URL
|
|
2239
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
|
+
}
|
|
2240
2301
|
return { remote, status: "disabled" };
|
|
2241
2302
|
}
|
|
2242
2303
|
async function undisabledRemotes(cwd) {
|
|
@@ -2277,7 +2338,7 @@ async function restoreOneRemote(cwd, remote) {
|
|
|
2277
2338
|
hadNone = false;
|
|
2278
2339
|
}
|
|
2279
2340
|
if (hadNone) {
|
|
2280
|
-
await
|
|
2341
|
+
await unsetIgnoringMissing(cwd, `remote.${remote}.pushurl`);
|
|
2281
2342
|
} else {
|
|
2282
2343
|
await runGit(cwd, [
|
|
2283
2344
|
"config",
|
|
@@ -2292,12 +2353,19 @@ async function restoreOneRemote(cwd, remote) {
|
|
|
2292
2353
|
}
|
|
2293
2354
|
for (const staleKey of [key, NO_PUSH_URL_MARKER(remote), `arcane.originalPushUrl.${remote}`]) {
|
|
2294
2355
|
try {
|
|
2295
|
-
await
|
|
2356
|
+
await unsetIgnoringMissing(cwd, staleKey);
|
|
2296
2357
|
} catch {
|
|
2297
2358
|
}
|
|
2298
2359
|
}
|
|
2299
2360
|
return { remote, status: "restored" };
|
|
2300
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
|
+
}
|
|
2301
2369
|
async function hadNoPushUrl(cwd, remote) {
|
|
2302
2370
|
return (await readLocalAll(cwd, NO_PUSH_URL_MARKER(remote)))[0] === "true";
|
|
2303
2371
|
}
|
|
@@ -2659,6 +2727,10 @@ async function runInit(options, targetDir, assetsDir, packageVersion) {
|
|
|
2659
2727
|
printWarning(
|
|
2660
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`."
|
|
2661
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
|
+
);
|
|
2662
2734
|
} else if (hook.status === "refused-foreign-hooks-path") {
|
|
2663
2735
|
printWarning(
|
|
2664
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.`
|
|
@@ -2819,7 +2891,7 @@ import { join as join8 } from "path";
|
|
|
2819
2891
|
|
|
2820
2892
|
// src/modules/hub.ts
|
|
2821
2893
|
import { confirm as confirm2, select as select3, input as input3 } from "@inquirer/prompts";
|
|
2822
|
-
import { readdir as
|
|
2894
|
+
import { readdir as readdir4, writeFile as writeFile6 } from "fs/promises";
|
|
2823
2895
|
import { join as join7 } from "path";
|
|
2824
2896
|
var MANIFEST_RETROFITS = [
|
|
2825
2897
|
{
|
|
@@ -2963,7 +3035,7 @@ async function offerRegistryScaffold(targetDir, businessRoot) {
|
|
|
2963
3035
|
const businessRootPath = join7(targetDir, businessRoot);
|
|
2964
3036
|
let ventureDirs;
|
|
2965
3037
|
try {
|
|
2966
|
-
const entries = await
|
|
3038
|
+
const entries = await readdir4(businessRootPath, { withFileTypes: true });
|
|
2967
3039
|
ventureDirs = entries.filter((e) => e.isDirectory() && e.name !== "_template").map((e) => e.name);
|
|
2968
3040
|
} catch {
|
|
2969
3041
|
return;
|
|
@@ -3170,7 +3242,7 @@ async function runUpdate(options, targetDir, assetsDir, packageVersion) {
|
|
|
3170
3242
|
|
|
3171
3243
|
// src/modules/version-check.ts
|
|
3172
3244
|
import { readFile as readFile5, writeFile as writeFile7, mkdir as mkdir5 } from "fs/promises";
|
|
3173
|
-
import { dirname, join as join9 } from "path";
|
|
3245
|
+
import { dirname as dirname2, join as join9 } from "path";
|
|
3174
3246
|
import { homedir as homedir2 } from "os";
|
|
3175
3247
|
import { createRequire } from "module";
|
|
3176
3248
|
var require2 = createRequire(import.meta.url);
|
|
@@ -3199,7 +3271,7 @@ async function readCache(cachePath) {
|
|
|
3199
3271
|
}
|
|
3200
3272
|
}
|
|
3201
3273
|
async function writeCache(cachePath, latest) {
|
|
3202
|
-
await mkdir5(
|
|
3274
|
+
await mkdir5(dirname2(cachePath), { recursive: true });
|
|
3203
3275
|
const entry = { latest, checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
3204
3276
|
await writeFile7(cachePath, JSON.stringify(entry, null, 2), "utf-8");
|
|
3205
3277
|
}
|
|
@@ -3409,7 +3481,7 @@ Run \`spell unblock-push\` first (interactive), then uninstall.`
|
|
|
3409
3481
|
|
|
3410
3482
|
// src/commands/doctor.ts
|
|
3411
3483
|
import { access as access4, readFile as readFile6, mkdir as mkdir6, copyFile as fsCopyFile2 } from "fs/promises";
|
|
3412
|
-
import { join as join11, dirname as
|
|
3484
|
+
import { join as join11, dirname as dirname3 } from "path";
|
|
3413
3485
|
import { execFile as execFile2 } from "child_process";
|
|
3414
3486
|
import { promisify } from "util";
|
|
3415
3487
|
|
|
@@ -3502,8 +3574,8 @@ async function checkVSCodeExtension(id, label) {
|
|
|
3502
3574
|
];
|
|
3503
3575
|
for (const dir of extDirs) {
|
|
3504
3576
|
try {
|
|
3505
|
-
const { readdir:
|
|
3506
|
-
const entries = await
|
|
3577
|
+
const { readdir: readdir5 } = await import("fs/promises");
|
|
3578
|
+
const entries = await readdir5(dir);
|
|
3507
3579
|
const prefix = id.toLowerCase() + "-";
|
|
3508
3580
|
const found = entries.some(
|
|
3509
3581
|
(e) => e.toLowerCase() === id.toLowerCase() || e.toLowerCase().startsWith(prefix)
|
|
@@ -3651,7 +3723,7 @@ async function fixSessionContinuity(targetDir, assetsDir) {
|
|
|
3651
3723
|
await access4(destPath);
|
|
3652
3724
|
} catch {
|
|
3653
3725
|
const srcPath = join11(assetsDir, file);
|
|
3654
|
-
await mkdir6(
|
|
3726
|
+
await mkdir6(dirname3(destPath), { recursive: true });
|
|
3655
3727
|
await fsCopyFile2(srcPath, destPath);
|
|
3656
3728
|
created.push(file);
|
|
3657
3729
|
}
|
|
@@ -3762,7 +3834,7 @@ async function runDoctor(targetDir, options = {}, assetsDir) {
|
|
|
3762
3834
|
}
|
|
3763
3835
|
|
|
3764
3836
|
// src/commands/unblock-push.ts
|
|
3765
|
-
import { basename, resolve as resolve2 } from "path";
|
|
3837
|
+
import { basename as basename2, resolve as resolve2 } from "path";
|
|
3766
3838
|
import { input as input4 } from "@inquirer/prompts";
|
|
3767
3839
|
async function runUnblockPush(targetDir) {
|
|
3768
3840
|
let manifest;
|
|
@@ -3777,10 +3849,16 @@ async function runUnblockPush(targetDir) {
|
|
|
3777
3849
|
throw err;
|
|
3778
3850
|
}
|
|
3779
3851
|
const policy = manifest.push_policy ?? "open";
|
|
3780
|
-
|
|
3852
|
+
const stillInForce = await isHookEnforced(targetDir) || (await blockedRemotes(targetDir)).length > 0;
|
|
3853
|
+
if (policy === "open" && !stillInForce) {
|
|
3781
3854
|
printInfo("This repository is not push-blocked \u2014 nothing to undo.");
|
|
3782
3855
|
return;
|
|
3783
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
|
+
}
|
|
3784
3862
|
if (!process.stdin.isTTY) {
|
|
3785
3863
|
console.error(
|
|
3786
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."
|
|
@@ -3788,7 +3866,7 @@ async function runUnblockPush(targetDir) {
|
|
|
3788
3866
|
process.exit(1);
|
|
3789
3867
|
return;
|
|
3790
3868
|
}
|
|
3791
|
-
const repoName =
|
|
3869
|
+
const repoName = basename2(resolve2(targetDir));
|
|
3792
3870
|
if (repoName === "") {
|
|
3793
3871
|
console.error(
|
|
3794
3872
|
"Could not determine a repository name to confirm against (is this repository at a filesystem root?). Refusing to unblock."
|
|
@@ -3857,7 +3935,7 @@ Name did not match ("${typed.trim()}" \u2260 "${repoName}"). Nothing changed.`);
|
|
|
3857
3935
|
import { realpathSync } from "fs";
|
|
3858
3936
|
var require3 = createRequire2(import.meta.url);
|
|
3859
3937
|
var pkg = require3("../package.json");
|
|
3860
|
-
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");
|
|
3861
3939
|
var program = new Command();
|
|
3862
3940
|
program.name("spell").description("Arcane framework CLI \u2014 scaffold and manage governance files").version(pkg.version).action(async () => {
|
|
3863
3941
|
await printWelcome(pkg.version, process.cwd());
|