arcane-cli 1.11.4 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/.arcane/image-prompts/adelaide-image-prompt.md +1 -1
- package/dist/assets/.arcane/image-prompts/alexander-image-prompt.md +1 -1
- package/dist/assets/.arcane/image-prompts/bess-image-prompt.md +1 -1
- package/dist/assets/.arcane/image-prompts/circe-image-prompt.md +1 -1
- package/dist/assets/.arcane/image-prompts/custodio-image-prompt.md +1 -1
- package/dist/assets/.arcane/image-prompts/iris-image-prompt.md +1 -1
- package/dist/assets/.arcane/image-prompts/kellar-image-prompt.md +1 -1
- package/dist/assets/.arcane/image-prompts/lafayette-image-prompt.md +1 -1
- package/dist/assets/.arcane/image-prompts/lince-image-prompt.md +1 -1
- package/dist/assets/.arcane/image-prompts/mercurio-image-prompt.md +1 -1
- package/dist/assets/.arcane/image-prompts/merlin-image-prompt.md +1 -1
- package/dist/assets/.arcane/image-prompts/prospero-image-prompt.md +1 -1
- package/dist/assets/sign-in/NOTICE.txt +16 -0
- package/dist/assets/sign-in/OFL-chakra-petch.txt +93 -0
- package/dist/assets/sign-in/OFL-rajdhani.txt +93 -0
- package/dist/assets/sign-in/sign-in-callback-denied.html +320 -0
- package/dist/assets/sign-in/sign-in-callback-failure.html +320 -0
- package/dist/assets/sign-in/sign-in-callback-success.html +320 -0
- package/dist/index.js +212 -76
- 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 createRequire3 } from "module";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
|
-
import { dirname as dirname12, join as
|
|
9
|
+
import { dirname as dirname12, join as join22 } from "path";
|
|
10
10
|
|
|
11
11
|
// src/commands/init.ts
|
|
12
12
|
import { select as select3, confirm as confirm2, input as input3 } from "@inquirer/prompts";
|
|
@@ -7715,21 +7715,25 @@ var ChunkedSecretStore = class {
|
|
|
7715
7715
|
};
|
|
7716
7716
|
|
|
7717
7717
|
// src/modules/identity-config.ts
|
|
7718
|
+
function defineIdentityEnvironment(config) {
|
|
7719
|
+
return {
|
|
7720
|
+
...config,
|
|
7721
|
+
discoveryUrl: `https://${config.signInHost}/${config.tenantId}/v2.0/.well-known/openid-configuration`
|
|
7722
|
+
};
|
|
7723
|
+
}
|
|
7718
7724
|
var IDENTITY_ENVIRONMENTS = {
|
|
7719
|
-
production: {
|
|
7725
|
+
production: defineIdentityEnvironment({
|
|
7720
7726
|
environment: "production",
|
|
7721
7727
|
tenantId: "9f8edbfb-291c-4c4a-b9cc-8820a04f8973",
|
|
7722
7728
|
signInHost: "arcaneai.ciamlogin.com",
|
|
7723
|
-
clientId: "7cd8f05d-9979-4458-8ca2-47c67748c41f"
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
development: {
|
|
7729
|
+
clientId: "7cd8f05d-9979-4458-8ca2-47c67748c41f"
|
|
7730
|
+
}),
|
|
7731
|
+
development: defineIdentityEnvironment({
|
|
7727
7732
|
environment: "development",
|
|
7728
7733
|
tenantId: "b365de0e-3026-4afe-bf5b-556fccb4ffcf",
|
|
7729
7734
|
signInHost: "arcaneaidev.ciamlogin.com",
|
|
7730
|
-
clientId: "651be720-83cc-452b-8346-abf20713ad12"
|
|
7731
|
-
|
|
7732
|
-
}
|
|
7735
|
+
clientId: "651be720-83cc-452b-8346-abf20713ad12"
|
|
7736
|
+
})
|
|
7733
7737
|
};
|
|
7734
7738
|
var SIGN_IN_SCOPES = ["openid", "profile", "email", "offline_access"];
|
|
7735
7739
|
var ENVIRONMENT_VARIABLE = "ARCANE_ENVIRONMENT";
|
|
@@ -7752,6 +7756,70 @@ function resolveIdentityEnvironment(env = process.env) {
|
|
|
7752
7756
|
import { createServer } from "http";
|
|
7753
7757
|
import { execFile as execFile3 } from "child_process";
|
|
7754
7758
|
import * as client from "openid-client";
|
|
7759
|
+
|
|
7760
|
+
// src/modules/result-pages.ts
|
|
7761
|
+
import { createHash as createHash3 } from "crypto";
|
|
7762
|
+
import { readFile as readFile16 } from "fs/promises";
|
|
7763
|
+
import { join as join17 } from "path";
|
|
7764
|
+
var RESULT_KINDS = ["success", "denied", "failure"];
|
|
7765
|
+
function resultPagePath(kind) {
|
|
7766
|
+
return join17("sign-in", `sign-in-callback-${kind}.html`);
|
|
7767
|
+
}
|
|
7768
|
+
function styleHashes(html) {
|
|
7769
|
+
return [...html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi)].map((match) => {
|
|
7770
|
+
const text = match[1].replace(/\r\n?/g, "\n");
|
|
7771
|
+
return `'sha256-${createHash3("sha256").update(text, "utf8").digest("base64")}'`;
|
|
7772
|
+
});
|
|
7773
|
+
}
|
|
7774
|
+
function resultPageHeaders(html) {
|
|
7775
|
+
const hashes = [...new Set(styleHashes(html))];
|
|
7776
|
+
return {
|
|
7777
|
+
"cache-control": "no-store",
|
|
7778
|
+
"content-security-policy": [
|
|
7779
|
+
"default-src 'none'",
|
|
7780
|
+
`style-src ${hashes.length > 0 ? hashes.join(" ") : "'none'"}`,
|
|
7781
|
+
"img-src data:",
|
|
7782
|
+
"font-src data:",
|
|
7783
|
+
"base-uri 'none'",
|
|
7784
|
+
"form-action 'none'",
|
|
7785
|
+
"frame-ancestors 'none'"
|
|
7786
|
+
].join("; "),
|
|
7787
|
+
"content-type": "text/html; charset=utf-8",
|
|
7788
|
+
"referrer-policy": "no-referrer",
|
|
7789
|
+
"x-content-type-options": "nosniff"
|
|
7790
|
+
};
|
|
7791
|
+
}
|
|
7792
|
+
function resultPage(html) {
|
|
7793
|
+
return { html, headers: resultPageHeaders(html) };
|
|
7794
|
+
}
|
|
7795
|
+
function plainPage(heading, next) {
|
|
7796
|
+
return resultPage(
|
|
7797
|
+
[
|
|
7798
|
+
'<!doctype html><html lang="en"><meta charset="utf-8">',
|
|
7799
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
7800
|
+
`<title>${heading} \u2014 Arcane</title>`,
|
|
7801
|
+
"<style>:root{color-scheme:light dark}body{font-family:system-ui,sans-serif;margin:3rem;line-height:1.5}</style>",
|
|
7802
|
+
`<body><h1>${heading}</h1><p>${next}</p></body></html>`
|
|
7803
|
+
].join("")
|
|
7804
|
+
);
|
|
7805
|
+
}
|
|
7806
|
+
var PLAIN_RESULT_PAGES = {
|
|
7807
|
+
success: plainPage("Signed in to Arcane", "Return to your terminal. Your browser can be closed now."),
|
|
7808
|
+
denied: plainPage("Sign-in was not completed", "Return to your terminal to try again. No new session was stored."),
|
|
7809
|
+
failure: plainPage("Could not finish sign-in", "Return to your terminal for details. No new session was stored.")
|
|
7810
|
+
};
|
|
7811
|
+
async function loadResultPages(assetsDir) {
|
|
7812
|
+
try {
|
|
7813
|
+
const [success, denied, failure] = await Promise.all(
|
|
7814
|
+
RESULT_KINDS.map(async (kind) => resultPage(await readFile16(join17(assetsDir, resultPagePath(kind)), "utf8")))
|
|
7815
|
+
);
|
|
7816
|
+
return { success, denied, failure };
|
|
7817
|
+
} catch {
|
|
7818
|
+
return null;
|
|
7819
|
+
}
|
|
7820
|
+
}
|
|
7821
|
+
|
|
7822
|
+
// src/modules/oidc.ts
|
|
7755
7823
|
var IdentityUnreachableError = class extends Error {
|
|
7756
7824
|
constructor(host, cause) {
|
|
7757
7825
|
super(`Could not reach the identity service at ${host}.`);
|
|
@@ -7765,6 +7833,12 @@ var LoginTimeoutError = class extends Error {
|
|
|
7765
7833
|
this.name = "LoginTimeoutError";
|
|
7766
7834
|
}
|
|
7767
7835
|
};
|
|
7836
|
+
var LoginBrowserUnavailableError = class extends Error {
|
|
7837
|
+
constructor() {
|
|
7838
|
+
super("No browser could be opened. Run `spell login --device-code` and finish signing in on another device.");
|
|
7839
|
+
this.name = "LoginBrowserUnavailableError";
|
|
7840
|
+
}
|
|
7841
|
+
};
|
|
7768
7842
|
var LoginDeniedError = class extends Error {
|
|
7769
7843
|
constructor(code) {
|
|
7770
7844
|
super(`The identity service refused the sign-in (${code}).`);
|
|
@@ -7807,16 +7881,8 @@ function toTokenResult(tokens, previousRefreshToken) {
|
|
|
7807
7881
|
expiresAt: new Date(now.getTime() + seconds * 1e3)
|
|
7808
7882
|
};
|
|
7809
7883
|
}
|
|
7810
|
-
var
|
|
7811
|
-
|
|
7812
|
-
'<body style="font-family:system-ui;margin:3rem"><h1>Signed in to Arcane</h1>',
|
|
7813
|
-
"<p>You can close this window and go back to the terminal.</p></body>"
|
|
7814
|
-
].join("");
|
|
7815
|
-
var FAILED_PAGE = [
|
|
7816
|
-
'<!doctype html><meta charset="utf-8"><title>Arcane</title>',
|
|
7817
|
-
'<body style="font-family:system-ui;margin:3rem"><h1>Sign-in not completed</h1>',
|
|
7818
|
-
"<p>Go back to the terminal for what to do next.</p></body>"
|
|
7819
|
-
].join("");
|
|
7884
|
+
var resultPath = (kind) => `/result/${kind}`;
|
|
7885
|
+
var RESULT_PATHS = new Set(RESULT_KINDS.map(resultPath));
|
|
7820
7886
|
async function browserLogin(config, options) {
|
|
7821
7887
|
const codeVerifier = client.randomPKCECodeVerifier();
|
|
7822
7888
|
const codeChallenge = await client.calculatePKCECodeChallenge(codeVerifier);
|
|
@@ -7841,8 +7907,11 @@ async function browserLogin(config, options) {
|
|
|
7841
7907
|
state,
|
|
7842
7908
|
nonce
|
|
7843
7909
|
});
|
|
7910
|
+
const pages = options.pages ?? PLAIN_RESULT_PAGES;
|
|
7844
7911
|
const timeoutMs = options.timeoutMs ?? 5 * 60 * 1e3;
|
|
7845
7912
|
let timer;
|
|
7913
|
+
let callbackConsumed = false;
|
|
7914
|
+
let browserResult;
|
|
7846
7915
|
const close = () => {
|
|
7847
7916
|
if (timer) clearTimeout(timer);
|
|
7848
7917
|
server.closeAllConnections?.();
|
|
@@ -7851,14 +7920,53 @@ async function browserLogin(config, options) {
|
|
|
7851
7920
|
const tokens = new Promise((resolve5, reject) => {
|
|
7852
7921
|
server.on("request", (req, res) => {
|
|
7853
7922
|
const url = new URL(req.url ?? "/", redirectUri);
|
|
7923
|
+
if (RESULT_PATHS.has(url.pathname)) {
|
|
7924
|
+
if (!browserResult || url.pathname !== resultPath(browserResult.kind) || url.search !== "") {
|
|
7925
|
+
res.writeHead(404).end();
|
|
7926
|
+
return;
|
|
7927
|
+
}
|
|
7928
|
+
const served = browserResult;
|
|
7929
|
+
const page = pages[served.kind];
|
|
7930
|
+
const settle = () => {
|
|
7931
|
+
if (served.kind === "success") resolve5(served.result);
|
|
7932
|
+
else reject(served.error);
|
|
7933
|
+
};
|
|
7934
|
+
res.once("finish", settle).once("close", settle);
|
|
7935
|
+
res.writeHead(200, page.headers).end(page.html);
|
|
7936
|
+
return;
|
|
7937
|
+
}
|
|
7854
7938
|
if (url.pathname !== "/" || !url.searchParams.has("code") && !url.searchParams.has("error")) {
|
|
7855
7939
|
res.writeHead(404).end();
|
|
7856
7940
|
return;
|
|
7857
7941
|
}
|
|
7942
|
+
if (callbackConsumed) {
|
|
7943
|
+
res.writeHead(409, { "cache-control": "no-store" }).end();
|
|
7944
|
+
return;
|
|
7945
|
+
}
|
|
7946
|
+
if (url.searchParams.get("state") !== state) {
|
|
7947
|
+
res.writeHead(400, pages.failure.headers).end(pages.failure.html);
|
|
7948
|
+
return;
|
|
7949
|
+
}
|
|
7950
|
+
callbackConsumed = true;
|
|
7951
|
+
const redirectToResult = (result) => {
|
|
7952
|
+
browserResult = result;
|
|
7953
|
+
res.writeHead(303, {
|
|
7954
|
+
"cache-control": "no-store",
|
|
7955
|
+
location: resultPath(result.kind),
|
|
7956
|
+
"referrer-policy": "no-referrer"
|
|
7957
|
+
}).end();
|
|
7958
|
+
if (result.kind === "success") {
|
|
7959
|
+
if (timer) clearTimeout(timer);
|
|
7960
|
+
timer = setTimeout(() => resolve5(result.result), Math.min(timeoutMs, 5e3));
|
|
7961
|
+
}
|
|
7962
|
+
};
|
|
7858
7963
|
const errorCode = url.searchParams.get("error");
|
|
7859
7964
|
if (errorCode) {
|
|
7860
|
-
|
|
7861
|
-
|
|
7965
|
+
const code = errorCode.replace(/[^a-z_]/gi, "");
|
|
7966
|
+
redirectToResult({
|
|
7967
|
+
kind: code === "access_denied" ? "denied" : "failure",
|
|
7968
|
+
error: new LoginDeniedError(code)
|
|
7969
|
+
});
|
|
7862
7970
|
return;
|
|
7863
7971
|
}
|
|
7864
7972
|
client.authorizationCodeGrant(config, url, {
|
|
@@ -7866,12 +7974,12 @@ async function browserLogin(config, options) {
|
|
|
7866
7974
|
expectedState: state,
|
|
7867
7975
|
expectedNonce: nonce,
|
|
7868
7976
|
idTokenExpected: true
|
|
7869
|
-
}).then((response) => {
|
|
7870
|
-
|
|
7871
|
-
|
|
7977
|
+
}).then(async (response) => {
|
|
7978
|
+
const result = toTokenResult(response);
|
|
7979
|
+
await options.onValidated?.(result);
|
|
7980
|
+
redirectToResult({ kind: "success", result });
|
|
7872
7981
|
}).catch((error) => {
|
|
7873
|
-
|
|
7874
|
-
reject(error);
|
|
7982
|
+
redirectToResult({ kind: "failure", error });
|
|
7875
7983
|
});
|
|
7876
7984
|
});
|
|
7877
7985
|
timer = setTimeout(() => reject(new LoginTimeoutError()), timeoutMs);
|
|
@@ -7880,7 +7988,10 @@ async function browserLogin(config, options) {
|
|
|
7880
7988
|
});
|
|
7881
7989
|
try {
|
|
7882
7990
|
const opened = await options.openUrl(authorizationUrl.href);
|
|
7883
|
-
if (!opened)
|
|
7991
|
+
if (!opened) {
|
|
7992
|
+
options.onCannotOpen?.();
|
|
7993
|
+
throw new LoginBrowserUnavailableError();
|
|
7994
|
+
}
|
|
7884
7995
|
return await tokens;
|
|
7885
7996
|
} finally {
|
|
7886
7997
|
close();
|
|
@@ -7912,7 +8023,7 @@ function openInDefaultBrowser(url) {
|
|
|
7912
8023
|
import { execFileSync } from "child_process";
|
|
7913
8024
|
import { mkdirSync, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
7914
8025
|
import { userInfo } from "os";
|
|
7915
|
-
import { dirname as dirname10, join as
|
|
8026
|
+
import { dirname as dirname10, join as join18 } from "path";
|
|
7916
8027
|
|
|
7917
8028
|
// src/modules/keychain-backend.ts
|
|
7918
8029
|
import { createRequire as createRequire2 } from "module";
|
|
@@ -7938,6 +8049,17 @@ function createKeychainBackend(service = KEYCHAIN_SERVICE) {
|
|
|
7938
8049
|
|
|
7939
8050
|
// src/modules/session-store.ts
|
|
7940
8051
|
var INSECURE_SESSION_FILE_NAME = "session-insecure.json";
|
|
8052
|
+
var SessionClearError = class extends Error {
|
|
8053
|
+
constructor(keychainError, removed) {
|
|
8054
|
+
super(keychainError.message);
|
|
8055
|
+
this.keychainError = keychainError;
|
|
8056
|
+
this.removed = removed;
|
|
8057
|
+
this.name = "SessionClearError";
|
|
8058
|
+
this.cause = keychainError;
|
|
8059
|
+
}
|
|
8060
|
+
keychainError;
|
|
8061
|
+
removed;
|
|
8062
|
+
};
|
|
7941
8063
|
function isIsoDate(value) {
|
|
7942
8064
|
return typeof value === "string" && !Number.isNaN(Date.parse(value));
|
|
7943
8065
|
}
|
|
@@ -8035,7 +8157,7 @@ function restrictToCurrentUser(filePath) {
|
|
|
8035
8157
|
});
|
|
8036
8158
|
}
|
|
8037
8159
|
function insecureSessionFilePath(homeDir) {
|
|
8038
|
-
return
|
|
8160
|
+
return join18(userTierRoot(homeDir), INSECURE_SESSION_FILE_NAME);
|
|
8039
8161
|
}
|
|
8040
8162
|
function createSessionStorage(options = {}) {
|
|
8041
8163
|
const keychain = new ChunkedSecretStore(options.keychainBackend ?? createKeychainBackend());
|
|
@@ -8081,8 +8203,9 @@ function createSessionStorage(options = {}) {
|
|
|
8081
8203
|
const removed = [];
|
|
8082
8204
|
let keychainError;
|
|
8083
8205
|
try {
|
|
8084
|
-
|
|
8206
|
+
const hadKeychainSession = keychain.load().state !== "signed-out";
|
|
8085
8207
|
keychain.clear();
|
|
8208
|
+
if (hadKeychainSession) removed.push("keychain");
|
|
8086
8209
|
} catch (error) {
|
|
8087
8210
|
keychainError = error;
|
|
8088
8211
|
}
|
|
@@ -8090,6 +8213,9 @@ function createSessionStorage(options = {}) {
|
|
|
8090
8213
|
fileBackend.remove();
|
|
8091
8214
|
removed.push("file");
|
|
8092
8215
|
}
|
|
8216
|
+
if (keychainError instanceof KeychainUnavailableError) {
|
|
8217
|
+
throw new SessionClearError(keychainError, removed);
|
|
8218
|
+
}
|
|
8093
8219
|
if (keychainError) throw keychainError;
|
|
8094
8220
|
return { removed };
|
|
8095
8221
|
}
|
|
@@ -8097,7 +8223,7 @@ function createSessionStorage(options = {}) {
|
|
|
8097
8223
|
}
|
|
8098
8224
|
|
|
8099
8225
|
// src/commands/login.ts
|
|
8100
|
-
function defaultLoginDeps() {
|
|
8226
|
+
function defaultLoginDeps(assetsDir) {
|
|
8101
8227
|
return {
|
|
8102
8228
|
storage: createSessionStorage(),
|
|
8103
8229
|
environment: () => resolveIdentityEnvironment(),
|
|
@@ -8105,10 +8231,12 @@ function defaultLoginDeps() {
|
|
|
8105
8231
|
browserLogin,
|
|
8106
8232
|
deviceLogin,
|
|
8107
8233
|
openUrl: openInDefaultBrowser,
|
|
8108
|
-
isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY)
|
|
8234
|
+
isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
8235
|
+
resultPages: () => loadResultPages(assetsDir)
|
|
8109
8236
|
};
|
|
8110
8237
|
}
|
|
8111
8238
|
var INSECURE_STORAGE_WARNING = "Insecure storage: the session is kept in a file protected only by your account's file permissions, not in the OS keychain. Run `spell logout` to remove it.";
|
|
8239
|
+
var MISSING_RESULT_PAGES_WARNING = "This arcane-cli install is missing its sign-in result pages (assets/sign-in/), so the browser will show a plain page. Reinstall the package to restore them.";
|
|
8112
8240
|
function safeErrorText(error) {
|
|
8113
8241
|
const message = error instanceof Error ? error.message : String(error);
|
|
8114
8242
|
return message.split("\n")[0].replace(/([?&](?:code|state|id_token|access_token|refresh_token)=)[^&\s]+/gi, "$1[redacted]");
|
|
@@ -8128,7 +8256,7 @@ function describeAccount(record) {
|
|
|
8128
8256
|
const who = record.email ? `${record.email} (${record.sub})` : record.sub;
|
|
8129
8257
|
return record.environment === "development" ? `${who} [development]` : who;
|
|
8130
8258
|
}
|
|
8131
|
-
async function runLogin(options, deps
|
|
8259
|
+
async function runLogin(options, deps) {
|
|
8132
8260
|
let environment;
|
|
8133
8261
|
try {
|
|
8134
8262
|
environment = deps.environment();
|
|
@@ -8157,51 +8285,51 @@ async function runLogin(options, deps = defaultLoginDeps()) {
|
|
|
8157
8285
|
process.exit(1);
|
|
8158
8286
|
return;
|
|
8159
8287
|
}
|
|
8160
|
-
let
|
|
8288
|
+
let record;
|
|
8289
|
+
const saveValidatedSession = (result) => {
|
|
8290
|
+
const next = toSessionRecord(result, environment);
|
|
8291
|
+
deps.storage.save(next, { insecure: Boolean(options.insecureStorage) });
|
|
8292
|
+
record = next;
|
|
8293
|
+
};
|
|
8161
8294
|
try {
|
|
8162
8295
|
if (options.deviceCode) {
|
|
8163
|
-
result = await deps.deviceLogin(config, {
|
|
8296
|
+
const result = await deps.deviceLogin(config, {
|
|
8164
8297
|
scopes: SIGN_IN_SCOPES,
|
|
8165
8298
|
prompt: (uri, code, seconds) => {
|
|
8166
8299
|
printInfo(`Open ${uri} on any device and enter the code ${code}.`);
|
|
8167
8300
|
printInfo(`Waiting up to ${Math.round(seconds / 60)} minutes for you to finish signing in.`);
|
|
8168
8301
|
}
|
|
8169
8302
|
});
|
|
8303
|
+
saveValidatedSession(result);
|
|
8170
8304
|
} else {
|
|
8305
|
+
const pages = await deps.resultPages();
|
|
8306
|
+
if (!pages) printWarning(MISSING_RESULT_PAGES_WARNING);
|
|
8171
8307
|
printInfo("Opening your browser to sign in to Arcane. Come back here when it says you are signed in.");
|
|
8172
|
-
|
|
8308
|
+
await deps.browserLogin(config, {
|
|
8173
8309
|
scopes: SIGN_IN_SCOPES,
|
|
8174
8310
|
openUrl: deps.openUrl,
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8311
|
+
pages: pages ?? void 0,
|
|
8312
|
+
onValidated: saveValidatedSession,
|
|
8313
|
+
onCannotOpen: () => {
|
|
8314
|
+
printWarning("No browser could be opened. Run `spell login --device-code` to sign in on another device.");
|
|
8178
8315
|
}
|
|
8179
8316
|
});
|
|
8317
|
+
if (!record) throw new Error("The browser sign-in ended without storing a session.");
|
|
8180
8318
|
}
|
|
8181
|
-
} catch (error) {
|
|
8182
|
-
if (error instanceof LoginTimeoutError || error instanceof LoginDeniedError) {
|
|
8183
|
-
console.error(`${error.message} Nothing was changed.`);
|
|
8184
|
-
} else {
|
|
8185
|
-
console.error(`The sign-in did not complete: ${safeErrorText(error)}. Nothing was changed.`);
|
|
8186
|
-
}
|
|
8187
|
-
process.exit(1);
|
|
8188
|
-
return;
|
|
8189
|
-
}
|
|
8190
|
-
const record = toSessionRecord(result, environment);
|
|
8191
|
-
try {
|
|
8192
|
-
deps.storage.save(record, { insecure: Boolean(options.insecureStorage) });
|
|
8193
8319
|
} catch (error) {
|
|
8194
8320
|
if (error instanceof KeychainUnavailableError) {
|
|
8195
8321
|
console.error(
|
|
8196
8322
|
`${error.message} Nothing was stored. If this machine has no keychain, run \`spell login --insecure-storage\` to keep the session in a file instead (protected only by your account's file permissions).`
|
|
8197
8323
|
);
|
|
8198
|
-
|
|
8199
|
-
|
|
8324
|
+
} else if (error instanceof LoginTimeoutError || error instanceof LoginDeniedError) {
|
|
8325
|
+
console.error(`${error.message} Nothing was changed.`);
|
|
8326
|
+
} else {
|
|
8327
|
+
console.error(`The sign-in did not complete: ${safeErrorText(error)}. Nothing was changed.`);
|
|
8200
8328
|
}
|
|
8201
|
-
console.error(`The session could not be stored: ${safeErrorText(error)}`);
|
|
8202
8329
|
process.exit(1);
|
|
8203
8330
|
return;
|
|
8204
8331
|
}
|
|
8332
|
+
if (!record) throw new Error("The sign-in completed without a stored session.");
|
|
8205
8333
|
printSuccess(`Signed in as ${describeAccount(record)}.`);
|
|
8206
8334
|
if (options.insecureStorage) printWarning(INSECURE_STORAGE_WARNING);
|
|
8207
8335
|
}
|
|
@@ -8212,6 +8340,14 @@ function runLogout(storage = createSessionStorage()) {
|
|
|
8212
8340
|
try {
|
|
8213
8341
|
removed = storage.clear().removed;
|
|
8214
8342
|
} catch (error) {
|
|
8343
|
+
if (error instanceof SessionClearError) {
|
|
8344
|
+
const fileResult = error.removed.includes("file") ? "The insecure session file was removed. " : "";
|
|
8345
|
+
console.error(
|
|
8346
|
+
`${fileResult}${error.keychainError.message} Any session in the OS keychain could not be verified or removed.`
|
|
8347
|
+
);
|
|
8348
|
+
process.exit(1);
|
|
8349
|
+
return;
|
|
8350
|
+
}
|
|
8215
8351
|
if (error instanceof KeychainUnavailableError) {
|
|
8216
8352
|
console.error(`${error.message} The session could not be removed from it.`);
|
|
8217
8353
|
process.exit(1);
|
|
@@ -8289,15 +8425,15 @@ async function runWhoami(options, deps = defaultWhoamiDeps()) {
|
|
|
8289
8425
|
|
|
8290
8426
|
// src/commands/report.ts
|
|
8291
8427
|
import { access as access5 } from "fs/promises";
|
|
8292
|
-
import { join as
|
|
8428
|
+
import { join as join21 } from "path";
|
|
8293
8429
|
|
|
8294
8430
|
// src/modules/show-report/generate.ts
|
|
8295
|
-
import { mkdir as mkdir8, readdir as readdir11, readFile as
|
|
8296
|
-
import { basename as basename3, dirname as dirname11, isAbsolute, join as
|
|
8431
|
+
import { mkdir as mkdir8, readdir as readdir11, readFile as readFile18, writeFile as writeFile12 } from "fs/promises";
|
|
8432
|
+
import { basename as basename3, dirname as dirname11, isAbsolute, join as join20, relative as relative3, resolve as resolve4 } from "path";
|
|
8297
8433
|
|
|
8298
8434
|
// src/modules/show-report/model.ts
|
|
8299
|
-
import { readFile as
|
|
8300
|
-
import { join as
|
|
8435
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
8436
|
+
import { join as join19 } from "path";
|
|
8301
8437
|
|
|
8302
8438
|
// src/modules/show-report/plan-parser.ts
|
|
8303
8439
|
function parseFrontmatter(content) {
|
|
@@ -8570,7 +8706,7 @@ async function getCast(cwd, fromRef, toRef, generatedOutputs = []) {
|
|
|
8570
8706
|
// src/modules/show-report/model.ts
|
|
8571
8707
|
async function readOptional(path4) {
|
|
8572
8708
|
try {
|
|
8573
|
-
return await
|
|
8709
|
+
return await readFile17(path4, "utf8");
|
|
8574
8710
|
} catch {
|
|
8575
8711
|
return null;
|
|
8576
8712
|
}
|
|
@@ -8666,13 +8802,13 @@ function buildCorrections(ledgerRows, programName) {
|
|
|
8666
8802
|
};
|
|
8667
8803
|
}
|
|
8668
8804
|
async function buildShowReportModel(options) {
|
|
8669
|
-
const planAbsPath =
|
|
8670
|
-
const queueAbsPath =
|
|
8805
|
+
const planAbsPath = join19(options.rootDir, options.planRelPath);
|
|
8806
|
+
const queueAbsPath = join19(options.rootDir, options.queueRelPath);
|
|
8671
8807
|
const ledgerRelPath = "docs/verification-ledger.md";
|
|
8672
|
-
const ledgerAbsPath =
|
|
8673
|
-
const decisionsAbsPath =
|
|
8808
|
+
const ledgerAbsPath = join19(options.rootDir, ledgerRelPath);
|
|
8809
|
+
const decisionsAbsPath = join19(options.rootDir, "DECISIONS.md");
|
|
8674
8810
|
const [planContent, queueContent, ledgerContent, decisionsContent] = await Promise.all([
|
|
8675
|
-
|
|
8811
|
+
readFile17(planAbsPath, "utf8"),
|
|
8676
8812
|
readOptional(queueAbsPath),
|
|
8677
8813
|
readOptional(ledgerAbsPath),
|
|
8678
8814
|
readOptional(decisionsAbsPath)
|
|
@@ -8868,7 +9004,7 @@ function toPosix(path4) {
|
|
|
8868
9004
|
async function programFromPlan(rootDir, slug, planRelPath) {
|
|
8869
9005
|
let planContent;
|
|
8870
9006
|
try {
|
|
8871
|
-
planContent = await
|
|
9007
|
+
planContent = await readFile18(join20(rootDir, planRelPath), "utf8");
|
|
8872
9008
|
} catch {
|
|
8873
9009
|
return { skipped: { slug, reason: `no PLAN.md at ${planRelPath}` } };
|
|
8874
9010
|
}
|
|
@@ -8880,13 +9016,13 @@ async function programFromPlan(rootDir, slug, planRelPath) {
|
|
|
8880
9016
|
program: {
|
|
8881
9017
|
slug,
|
|
8882
9018
|
planRelPath,
|
|
8883
|
-
queueRelPath: toPosix(
|
|
9019
|
+
queueRelPath: toPosix(join20(dirname11(planRelPath), "OPERATOR-QUEUE.md")),
|
|
8884
9020
|
ledgerProgramName: programNameFromTitle(frontmatter.title ?? slug)
|
|
8885
9021
|
}
|
|
8886
9022
|
};
|
|
8887
9023
|
}
|
|
8888
9024
|
async function discoverPrograms(rootDir) {
|
|
8889
|
-
const plansDir =
|
|
9025
|
+
const plansDir = join20(rootDir, "docs", "plans");
|
|
8890
9026
|
let entries;
|
|
8891
9027
|
try {
|
|
8892
9028
|
entries = (await readdir11(plansDir, { withFileTypes: true })).filter((e) => e.isDirectory());
|
|
@@ -8913,14 +9049,14 @@ function normalizeLineEndings2(content) {
|
|
|
8913
9049
|
}
|
|
8914
9050
|
async function readOptionalFile(path4) {
|
|
8915
9051
|
try {
|
|
8916
|
-
return await
|
|
9052
|
+
return await readFile18(path4, "utf8");
|
|
8917
9053
|
} catch (error) {
|
|
8918
9054
|
if (error.code === "ENOENT") return null;
|
|
8919
9055
|
throw error;
|
|
8920
9056
|
}
|
|
8921
9057
|
}
|
|
8922
9058
|
async function generateReport(rootDir, program2, templateHtml, theme = "auto") {
|
|
8923
|
-
const planContent = await
|
|
9059
|
+
const planContent = await readFile18(join20(rootDir, program2.planRelPath), "utf8");
|
|
8924
9060
|
const frontmatter = parseFrontmatter(planContent);
|
|
8925
9061
|
const model = await buildShowReportModel({
|
|
8926
9062
|
rootDir,
|
|
@@ -8940,7 +9076,7 @@ async function generateReport(rootDir, program2, templateHtml, theme = "auto") {
|
|
|
8940
9076
|
};
|
|
8941
9077
|
}
|
|
8942
9078
|
async function runReportGeneration(options) {
|
|
8943
|
-
const templateHtml = await
|
|
9079
|
+
const templateHtml = await readFile18(options.templatePath, "utf8");
|
|
8944
9080
|
const theme = options.theme ?? "auto";
|
|
8945
9081
|
const drifted = [];
|
|
8946
9082
|
const repaired = [];
|
|
@@ -8958,12 +9094,12 @@ async function runReportGeneration(options) {
|
|
|
8958
9094
|
[`${outRel}/show-report.html`, generated.html]
|
|
8959
9095
|
];
|
|
8960
9096
|
for (const [relPath, expected] of outputs) {
|
|
8961
|
-
const actual = await readOptionalFile(
|
|
9097
|
+
const actual = await readOptionalFile(join20(options.rootDir, relPath));
|
|
8962
9098
|
if (actual === null || normalizeLineEndings2(actual) !== normalizeLineEndings2(expected)) {
|
|
8963
9099
|
drifted.push(`${relPath} does not match a regeneration from its sources.`);
|
|
8964
9100
|
if (options.mode === "write") {
|
|
8965
9101
|
await mkdir8(outAbs, { recursive: true });
|
|
8966
|
-
await writeFile12(
|
|
9102
|
+
await writeFile12(join20(options.rootDir, relPath), expected, "utf8");
|
|
8967
9103
|
repaired.push(relPath);
|
|
8968
9104
|
}
|
|
8969
9105
|
}
|
|
@@ -8974,7 +9110,7 @@ async function runReportGeneration(options) {
|
|
|
8974
9110
|
|
|
8975
9111
|
// src/commands/report.ts
|
|
8976
9112
|
async function runReport(targetDir, options = {}, assetsDir) {
|
|
8977
|
-
const templatePath =
|
|
9113
|
+
const templatePath = join21(assetsDir, "report", "show-report.template.html");
|
|
8978
9114
|
try {
|
|
8979
9115
|
await access5(templatePath);
|
|
8980
9116
|
} catch {
|
|
@@ -9051,7 +9187,7 @@ async function runReport(targetDir, options = {}, assetsDir) {
|
|
|
9051
9187
|
import { realpathSync } from "fs";
|
|
9052
9188
|
var require3 = createRequire3(import.meta.url);
|
|
9053
9189
|
var pkg = require3("../package.json");
|
|
9054
|
-
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ??
|
|
9190
|
+
var ASSETS_DIR = process.env["ARCANE_ASSETS_DIR"] ?? join22(dirname12(fileURLToPath(import.meta.url)), "assets");
|
|
9055
9191
|
var program = new Command();
|
|
9056
9192
|
var USER_FLAG_DESCRIPTION = "Operate on the per-user tier at ~/.arcane \u2014 spells shared by every repository on this machine, fanned out to ~/.agents/skills (Codex, Copilot) and ~/.claude/commands (Claude Code)";
|
|
9057
9193
|
var USER_TIER_AGENTS_DESCRIPTION = "Operate on the per-user tier at ~/.arcane \u2014 one agent roster shared by every repository on this machine, rendered to ~/.copilot/agents (VS Code agent modes)";
|
|
@@ -9178,7 +9314,7 @@ program.command("login").description("Sign in to your Arcane account (browser by
|
|
|
9178
9314
|
"--insecure-storage",
|
|
9179
9315
|
"Keep the session in a file under ~/.arcane instead of the OS keychain; protected only by your account's file permissions"
|
|
9180
9316
|
).action(async (opts) => {
|
|
9181
|
-
await runLogin({ deviceCode: opts.deviceCode, insecureStorage: opts.insecureStorage });
|
|
9317
|
+
await runLogin({ deviceCode: opts.deviceCode, insecureStorage: opts.insecureStorage }, defaultLoginDeps(ASSETS_DIR));
|
|
9182
9318
|
});
|
|
9183
9319
|
program.command("logout").description("Sign out on this machine: remove the stored session").action(() => {
|
|
9184
9320
|
runLogout();
|