Skip to content

Commit 39fc1d7

Browse files
feat: move CLI config to cross-platform config directory (#130)
* feat: move CLI config to cross-platform config directory Move CLI state from ~/.prismic to ~/.config/prismic/ with separate files per concern (credentials.json, update-notifier.json). Drop Slice Machine auth format compatibility (base/cookies fields, checkIsSliceMachineProject branching). Support PRISMIC_CONFIG_DIR env var override. Resolves #126 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: reorganize config, project, and auth file responsibilities - Merge src/config.ts (prismic.config.json CRUD) into src/project.ts since both are project-level concerns - Recreate src/config.ts as the CLI config directory module (CREDENTIALS_PATH, UPDATE_NOTIFIER_STATE_PATH) - src/auth.ts now imports paths from src/config.ts instead of owning them - Update all import paths across consumers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove section headings and add Error suffix to custom errors Rename: InvalidPrismicConfig, MissingPrismicConfig, UnknownProjectRoot, InvalidLegacySliceMachineConfig, MissingLegacySliceMachineConfig — all now have an Error suffix. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use PRISMIC_CONFIG_DIR in tests to avoid XDG_CONFIG_HOME mismatch on Linux Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: clean up stale ~/.prismic files with update-notifier properties The old CLI wrote latestKnownVersion/lastUpdateCheckAt into ~/.prismic, breaking Slice Machine's backward compatibility. On startup, detect and delete these files so they don't interfere. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7f75f97 commit 39fc1d7

17 files changed

Lines changed: 306 additions & 305 deletions

src/adapters/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
66
import { generateTypes } from "prismic-ts-codegen";
77
import { glob } from "tinyglobby";
88

9-
import { addRoute, removeRoute, updateRoute } from "../config";
9+
import { addRoute, removeRoute, updateRoute } from "../project";
1010
import { readJsonFile, writeFileRecursive } from "../lib/file";
1111
import { stringify } from "../lib/json";
1212
import { readPackageJson } from "../lib/packageJson";

src/adapters/nextjs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { relative } from "node:path";
66
import { fileURLToPath } from "node:url";
77

88
import { Adapter } from ".";
9-
import { buildRoutePath } from "../config";
9+
import { buildRoutePath } from "../project";
1010
import { exists, writeFileRecursive } from "../lib/file";
1111
import { addDependencies, findPackageJson, getNpmPackageVersion } from "../lib/packageJson";
1212
import { dedent } from "../lib/string";

src/adapters/nuxt.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { relative } from "node:path";
77
import { fileURLToPath } from "node:url";
88

99
import { Adapter } from ".";
10-
import { buildRoutePath, readConfig, updateConfig } from "../config";
10+
import { buildRoutePath, readConfig, updateConfig } from "../project";
1111
import { exists, writeFileRecursive } from "../lib/file";
1212
import { addDependencies, getNpmPackageVersion } from "../lib/packageJson";
1313
import { dedent } from "../lib/string";

src/adapters/sveltekit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { relative } from "node:path";
88
import { fileURLToPath } from "node:url";
99

1010
import { Adapter } from ".";
11-
import { buildRoutePath } from "../config";
11+
import { buildRoutePath } from "../project";
1212
import { exists, writeFileRecursive } from "../lib/file";
1313
import { addDependencies, findPackageJson, getNpmPackageVersion } from "../lib/packageJson";
1414
import { dedent } from "../lib/string";

src/auth.ts

Lines changed: 46 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,70 @@
1-
import { readFile, rm, writeFile } from "node:fs/promises";
1+
import { readFile, rm } from "node:fs/promises";
22
import { createServer } from "node:http";
33
import { homedir } from "node:os";
44
import { pathToFileURL } from "node:url";
55
import * as z from "zod/mini";
66

77
import { refreshToken as baseRefreshToken } from "./clients/auth";
8+
import { CREDENTIALS_PATH } from "./config";
89
import { DEFAULT_PRISMIC_HOST, env } from "./env";
9-
import { exists } from "./lib/file";
10+
import { exists, writeFileRecursive } from "./lib/file";
1011
import { stringify } from "./lib/json";
1112
import { appendTrailingSlash } from "./lib/url";
12-
import { checkIsSliceMachineProject } from "./project";
1313

14-
export const AUTH_FILE_PATH = new URL(".prismic", appendTrailingSlash(pathToFileURL(homedir())));
1514
const LOGIN_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
1615
const PREFERRED_PORT = 5555;
1716
const LOGIN_SOURCE = "prismic-cli";
1817

19-
const AuthFileSchema = z.looseObject({
18+
const CredentialsSchema = z.looseObject({
2019
token: z.optional(z.string().check(z.minLength(1))),
2120
host: z.optional(z.string().check(z.minLength(1))),
22-
23-
// Update notifier state
24-
latestKnownVersion: z.optional(z.string()),
25-
lastUpdateCheckAt: z.optional(z.number()),
26-
27-
// Backward compatibility with Slice Machine's .prismic
28-
base: z.optional(z.string()),
29-
cookies: z.optional(z.string()),
3021
});
31-
type AuthFile = z.infer<typeof AuthFileSchema>;
22+
type Credentials = z.infer<typeof CredentialsSchema>;
3223

3324
export async function getToken(): Promise<string | undefined> {
34-
const auth = await readAuthFile();
35-
const isSliceMachineProject = await checkIsSliceMachineProject();
36-
if (isSliceMachineProject && auth?.cookies) {
37-
for (const cookie of auth.cookies.split("; ")) {
38-
if (cookie.startsWith("prismic-auth=")) return cookie.replace(/^prismic-auth=/, "");
39-
}
40-
}
41-
return auth?.token;
25+
const credentials = await readCredentials();
26+
return credentials?.token;
4227
}
4328

4429
export async function getHost(): Promise<string> {
4530
if (env.PRISMIC_HOST) return env.PRISMIC_HOST;
46-
const auth = await readAuthFile();
47-
const isSliceMachineProject = await checkIsSliceMachineProject();
48-
if (isSliceMachineProject && auth?.base) {
49-
try {
50-
return new URL(auth.base).host || DEFAULT_PRISMIC_HOST;
51-
} catch {}
52-
}
53-
return auth?.host || DEFAULT_PRISMIC_HOST;
31+
const credentials = await readCredentials();
32+
return credentials?.host || DEFAULT_PRISMIC_HOST;
5433
}
5534

5635
export async function refreshToken(): Promise<string | undefined> {
5736
const token = await getToken();
5837
if (!token) return;
5938
const host = await getHost();
6039
const newToken = await baseRefreshToken(token, { host });
61-
await saveAuthFile({ token: newToken, host });
40+
await saveCredentials({ token: newToken, host });
6241
return newToken;
6342
}
6443

6544
export async function logout(): Promise<boolean> {
66-
const authFileExists = await exists(AUTH_FILE_PATH);
67-
if (!authFileExists) return true;
45+
const credentialsExist = await exists(CREDENTIALS_PATH);
46+
if (!credentialsExist) return true;
6847

6948
try {
70-
await rm(AUTH_FILE_PATH, { force: true });
49+
await rm(CREDENTIALS_PATH, { force: true });
7150
return true;
7251
} catch {
7352
return false;
7453
}
7554
}
7655

77-
export async function readAuthFile(): Promise<AuthFile | undefined> {
56+
async function readCredentials(): Promise<Credentials | undefined> {
7857
try {
79-
const contents = await readFile(AUTH_FILE_PATH, "utf-8");
58+
const contents = await readFile(CREDENTIALS_PATH, "utf-8");
8059
const json = JSON.parse(contents);
81-
return z.parse(AuthFileSchema, json);
60+
return z.parse(CredentialsSchema, json);
8261
} catch {
8362
return undefined;
8463
}
8564
}
8665

87-
export async function saveAuthFile(auth: AuthFile): Promise<void> {
88-
const existingAuthFile = await readAuthFile();
89-
const newAuthFile: AuthFile = { ...existingAuthFile, ...auth };
90-
const isSliceMachineProject = await checkIsSliceMachineProject();
91-
if (isSliceMachineProject) {
92-
if (newAuthFile.host) newAuthFile.base = `https://${newAuthFile.host}/`;
93-
if (newAuthFile.token)
94-
newAuthFile.cookies = `prismic-auth=${newAuthFile.token}; Path=/; SameSite=none; SESSION=fake_session; Path=/; SameSite=none`;
95-
}
96-
await writeFile(AUTH_FILE_PATH, stringify(newAuthFile));
66+
async function saveCredentials(credentials: Credentials): Promise<void> {
67+
await writeFileRecursive(CREDENTIALS_PATH, stringify(credentials));
9768
}
9869

9970
export async function createLoginSession(options?: {
@@ -139,7 +110,7 @@ export async function createLoginSession(options?: {
139110
return;
140111
}
141112

142-
await saveAuthFile({ token, host });
113+
await saveCredentials({ token, host });
143114

144115
res.writeHead(200, {
145116
"Access-Control-Allow-Origin": corsOrigin,
@@ -197,6 +168,33 @@ export async function createLoginSession(options?: {
197168
});
198169
}
199170

171+
const LEGACY_AUTH_FILE_PATH = new URL(
172+
".prismic",
173+
appendTrailingSlash(pathToFileURL(homedir())),
174+
);
175+
176+
export async function cleanupLegacyAuthFile(): Promise<void> {
177+
let contents: string;
178+
try {
179+
contents = await readFile(LEGACY_AUTH_FILE_PATH, "utf-8");
180+
} catch {
181+
return;
182+
}
183+
184+
try {
185+
const json = JSON.parse(contents);
186+
if (!json || (json.latestKnownVersion === undefined && json.lastUpdateCheckAt === undefined)) {
187+
return;
188+
}
189+
} catch {
190+
return;
191+
}
192+
193+
try {
194+
await rm(LEGACY_AUTH_FILE_PATH, { force: true });
195+
} catch {}
196+
}
197+
200198
async function buildLoginUrl(host: string, port: number): Promise<URL> {
201199
const url = new URL("dashboard/cli/login", `https://${host}/`);
202200
url.searchParams.set("source", LOGIN_SOURCE);

src/commands/init.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import { getProfile } from "../clients/user";
66
import {
77
createConfig,
88
deleteLegacySliceMachineConfig,
9-
InvalidLegacySliceMachineConfig,
10-
MissingPrismicConfig,
9+
InvalidLegacySliceMachineConfigError,
10+
MissingPrismicConfigError,
1111
readConfig,
1212
readLegacySliceMachineConfig,
13-
UnknownProjectRoot,
14-
} from "../config";
13+
UnknownProjectRootError,
14+
} from "../project";
1515
import { DEFAULT_PRISMIC_HOST } from "../env";
1616
import { openBrowser } from "../lib/browser";
1717
import { CommandError, createCommand, type CommandConfig } from "../lib/command";
@@ -50,7 +50,7 @@ export default createCommand(config, async ({ values }) => {
5050
"A prismic.config.json file exists. This project is already initialized.",
5151
);
5252
} catch (error) {
53-
if (error instanceof MissingPrismicConfig) {
53+
if (error instanceof MissingPrismicConfigError) {
5454
// No config found — proceed with initialization.
5555
} else {
5656
throw error;
@@ -62,7 +62,7 @@ export default createCommand(config, async ({ values }) => {
6262
try {
6363
legacySliceMachineConfig = await readLegacySliceMachineConfig();
6464
} catch (error) {
65-
if (error instanceof InvalidLegacySliceMachineConfig) {
65+
if (error instanceof InvalidLegacySliceMachineConfigError) {
6666
console.warn("Could not read slicemachine.config.json, ignoring.");
6767
}
6868
}
@@ -125,7 +125,7 @@ export default createCommand(config, async ({ values }) => {
125125
routes: [],
126126
});
127127
} catch (error) {
128-
if (error instanceof UnknownProjectRoot) {
128+
if (error instanceof UnknownProjectRootError) {
129129
throw new CommandError(
130130
"Could not find a package.json file. Run this command from a project directory.",
131131
);

0 commit comments

Comments
 (0)