diff --git a/README.md b/README.md
index 37145b6..11345f1 100644
--- a/README.md
+++ b/README.md
@@ -263,6 +263,19 @@ live with no restart (Req 9.4). A workspace's own
`samples/settings.json` (in this repository) is a working, commented
starting point covering every key below.
+Pass `--config
` at startup (e.g.
+`tecode --config /path/to/cfg ./my-project`) to read the user settings
+and user keybindings layers from `/settings.json` and
+`/keybindings.json` instead (Req 9.6) — useful for an isolated
+profile or a CI sandbox. Only the user layer moves; a workspace's own
+`.tecode/settings.json` still overlays on top exactly as above. A
+missing `` (or a missing file inside it) is treated the same as a
+missing home-directory file: an empty layer, not an error. A relative
+`` resolves against the current working directory. `--config` with
+no directory argument after it is ignored (no override applied), and it
+never consumes the directory/file argument that opens a workspace —
+`tecode --config /path/to/cfg ./my-project` still opens `./my-project`.
+
Req 9.5 names six MVP settings; the table marks which of them a real
`contributes.configuration` schema registers today, and which do not
exist yet:
diff --git a/design.md b/design.md
index c2c0063..3108f4c 100644
--- a/design.md
+++ b/design.md
@@ -254,6 +254,8 @@ Pipeline (*Req 8*), in `core` with languages contributed by extensions:
- **JSONC**: a small tolerant parser (strip comments + trailing commas, then `JSON.parse`) with error positions surfaced in the status bar; a broken file keeps the last good configuration.
- **Layering**: defaults (from `contributes.configuration` schemas and core defaults) ← user `settings.json` ← workspace `.tecode/settings.json`. `tecode.config.get(key)` reads the merged view; the schema registry supplies types/defaults and (later) validation.
- **Watch**: `fs.watch` on both settings files and `keybindings.json`; on change, re-parse, diff keys, fire `onDidChangeConfiguration({ affectsConfiguration })`, and notify dependent services (theme service on `workbench.colorTheme`, keymap service rebuilds its table) (*Req 9.4*).
+- **`--config ` override** (*Req 9.6*): the CLI's `--config ` flag (`cli/argv.ts`'s `resolveConfigDirOverride`) redirects the USER layer only — `/settings.json` and `/keybindings.json` replace the home-directory defaults `ConfigServiceDeps.settingsPath`/`keybindingsPath` otherwise fall back to (`host/paths.ts`'s `getUserSettingsPath`/`getUserKeybindingsPath`); the workspace layer's own resolution is untouched. A directory argument still following `--config ` opens as the workspace exactly as before this flag existed; `--config` with no directory argument at all opens no workspace, same as no arguments given.
+
## 12. Public API Assembly
diff --git a/packages/cli/src/argv.test.ts b/packages/cli/src/argv.test.ts
index e013e53..8c3b29f 100644
--- a/packages/cli/src/argv.test.ts
+++ b/packages/cli/src/argv.test.ts
@@ -3,7 +3,7 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { createHostLog, type HostLogEntry } from "@tecode/core";
-import { resolveStartupTarget } from "./argv";
+import { resolveConfigDirOverride, resolveStartupTarget } from "./argv";
let dir: string | undefined;
@@ -90,3 +90,84 @@ test("parent directory of a nested file resolves correctly", async () => {
expect(target.workspaceRoot).toBe(dirname(filePath));
expect(target.initialFilePath).toBe(filePath);
});
+
+// --- resolveConfigDirOverride (Req 9.6, Issue #81 Phase 1) ---
+
+test("resolveConfigDirOverride returns the token immediately after --config", () => {
+ expect(resolveConfigDirOverride(["--config", "/tmp/cfg"])).toBe("/tmp/cfg");
+});
+
+test("resolveConfigDirOverride returns undefined when --config is absent", () => {
+ expect(resolveConfigDirOverride([])).toBeUndefined();
+ expect(resolveConfigDirOverride(["./src"])).toBeUndefined();
+});
+
+test("resolveConfigDirOverride returns undefined when --config is the last token (no value follows)", () => {
+ expect(resolveConfigDirOverride(["--config"])).toBeUndefined();
+ expect(resolveConfigDirOverride(["./src", "--config"])).toBeUndefined();
+});
+
+test("resolveConfigDirOverride finds --config regardless of surrounding tokens", () => {
+ expect(resolveConfigDirOverride(["./src", "--config", "/tmp/cfg"])).toBe("/tmp/cfg");
+ expect(resolveConfigDirOverride(["--config", "/tmp/cfg", "./src"])).toBe("/tmp/cfg");
+});
+
+// --- resolveStartupTarget's --config non-confusion (Req 9.6, Issue #81 Phase 1) ---
+
+test("--config's value is not mistaken for the positional argument: a directory still follows it", async () => {
+ dir = await mkdtemp(join(tmpdir(), "tecode-argv-"));
+ const srcDir = join(dir, "src");
+ await mkdir(srcDir, { recursive: true });
+
+ const log = createHostLog();
+ const target = await resolveStartupTarget(
+ ["--config", "/tmp/some-config-dir", srcDir],
+ "/irrelevant",
+ log,
+ );
+ expect(target).toEqual({ workspaceRoot: srcDir });
+});
+
+test("a REPEATED --config's value is not mistaken for the positional argument either", async () => {
+ // Which `--config` wins is a separate question from which tokens are
+ // values. Excluding only the FIRST occurrence's value left the second one
+ // looking like a bare positional, so `--config /a --config /b` silently
+ // opened `/b` as the WORKSPACE — a different thing entirely from what was
+ // asked (CodeRabbit finding on PR #85).
+ const dir = await mkdtemp(join(tmpdir(), "tecode-argv-repeat-"));
+ const otherDir = await mkdtemp(join(tmpdir(), "tecode-argv-repeat-other-"));
+ const log = createHostLog();
+ try {
+ const target = await resolveStartupTarget(
+ ["--config", dir, "--config", otherDir],
+ process.cwd(),
+ log,
+ );
+ expect(target).toEqual({ workspaceRoot: process.cwd() });
+ // The override itself still takes the first occurrence.
+ expect(resolveConfigDirOverride(["--config", dir, "--config", otherDir])).toBe(dir);
+ } finally {
+ await rm(dir, { recursive: true, force: true });
+ await rm(otherDir, { recursive: true, force: true });
+ }
+});
+
+test("--config with no following positional opens nothing (falls back to cwd)", async () => {
+ const log = createHostLog();
+ const target = await resolveStartupTarget(
+ ["--config", "/tmp/some-config-dir"],
+ "/fallback-cwd",
+ log,
+ );
+ expect(target).toEqual({ workspaceRoot: "/fallback-cwd" });
+ // The config dir's value itself was never treated as a bad startup
+ // path — no warning should have been logged about it.
+ expect(log.entries()).toEqual([]);
+});
+
+test("a plain positional argument still opens normally when --config is entirely absent", async () => {
+ dir = await mkdtemp(join(tmpdir(), "tecode-argv-"));
+ const log = createHostLog();
+ const target = await resolveStartupTarget([dir], "/irrelevant", log);
+ expect(target).toEqual({ workspaceRoot: dir });
+});
diff --git a/packages/cli/src/argv.ts b/packages/cli/src/argv.ts
index ebeccaa..3080584 100644
--- a/packages/cli/src/argv.ts
+++ b/packages/cli/src/argv.ts
@@ -4,6 +4,10 @@
* first step; tasks.md's Task 1.15 "Argv parsing (file/directory)").
* `--version` is handled by `main.ts` itself, before this module is even
* reached (it must not touch the filesystem or build any services).
+ * `--config ` (Req 9.6, Issue #81 Phase 1) is parsed here too, by
+ * {@link resolveConfigDirOverride} — a separate, synchronous, pure helper
+ * (it does no I/O and never throws) that `main.ts` calls alongside
+ * {@link resolveStartupTarget}.
*/
import { stat as nodeStat } from "node:fs/promises";
@@ -50,6 +54,52 @@ function describeError(err: unknown): string {
}
}
+/** Every index in `argv` holding a `--config` flag's value — i.e. the token
+ * immediately after each `--config` occurrence (Issue #81 Phase 1). Shared
+ * by {@link resolveConfigDirOverride} and {@link resolveStartupTarget} so
+ * both agree on exactly which tokens are flag values rather than the
+ * positional argument.
+ *
+ * **Every occurrence, not just the first**: which `--config` *wins* is a
+ * separate question from which tokens are values. The override itself takes
+ * the first occurrence (see {@link resolveConfigDirOverride}, matching this
+ * module's "first token wins" treatment of the positional argument below),
+ * but a repeated flag's value must STILL be excluded from the positional
+ * scan. Considering only the first occurrence would leave the second value
+ * looking like a bare positional, so `tecode --config /a --config /b` would
+ * silently open `/b` as the workspace — a different thing entirely from
+ * what was asked (CodeRabbit finding on PR #85). */
+function findConfigValueIndices(argv: readonly string[]): ReadonlySet {
+ const indices = new Set();
+ for (const [index, arg] of argv.entries()) {
+ if (arg === "--config" && index + 1 < argv.length) indices.add(index + 1);
+ }
+ return indices;
+}
+
+/**
+ * Resolve `--config `'s value from argv (Req 9.6, design.md §11's
+ * `--config` note; Issue #81 Phase 1). Returns the token immediately
+ * following the first `--config` flag, or `undefined` when `--config` is
+ * absent from `argv` entirely, or when it is present but is the very last
+ * token (no value follows). Never throws (matches this module's
+ * never-throwing, degrade-to-`undefined` policy) — it does no I/O and
+ * cannot fail. Does not validate that the returned string names a real,
+ * readable directory; that check happens where the value is actually used
+ * (`@tecode/core`'s `ConfigService`, which degrades a missing/unreadable
+ * settings or keybindings file to an empty layer exactly as it does for
+ * the un-overridden home-directory default).
+ *
+ * `--version` is still handled by `main.ts` itself before this module (or
+ * `resolveStartupTarget`) ever sees argv (this module's top-of-file
+ * TSDoc) — nothing here needs to special-case it.
+ */
+export function resolveConfigDirOverride(argv: readonly string[]): string | undefined {
+ const flagIndex = argv.indexOf("--config");
+ if (flagIndex === -1) return undefined;
+ return argv[flagIndex + 1];
+}
+
/**
* Resolve the CLI's one positional argument (CodeRabbit's Phase 1 plan): a
* directory becomes `workspaceRoot` with no initial document; a file's
@@ -59,6 +109,17 @@ function describeError(err: unknown): string {
* the caller — this function only ever looks for the first token that
* does not start with `-`.
*
+ * **`--config `'s value is never mistaken for the positional
+ * argument** (Req 9.6, Issue #81 Phase 1): `--config`'s own value token
+ * (whatever immediately follows it, even a bare directory name with no
+ * leading `-`) is skipped when scanning for the positional, using the same
+ * {@link findConfigValueIndices} lookup {@link resolveConfigDirOverride}
+ * uses — so `tecode --config /tmp/cfg ./src` still opens `./src`, and
+ * `tecode --config /tmp/cfg` (no further token) opens nothing, exactly as
+ * if `--config /tmp/cfg` had been omitted. This function does not itself
+ * read or act on `--config`'s value — that is `resolveConfigDirOverride`'s
+ * job, called separately by `main.ts`.
+ *
* Never throws (matches `core`'s never-throwing service boundaries): a
* path that does not exist, or can't be `stat`-ed, is reported to `log` as
* a warning and treated as if no argument had been given (`cwd`) — a
@@ -72,7 +133,10 @@ export async function resolveStartupTarget(
log: HostLog,
fs: ArgvResolutionFs = createNodeArgvFs(),
): Promise {
- const positional = argv.find((arg) => !arg.startsWith("-"));
+ const configValueIndices = findConfigValueIndices(argv);
+ const positional = argv.find(
+ (arg, index) => !arg.startsWith("-") && !configValueIndices.has(index),
+ );
if (!positional) return { workspaceRoot: cwd };
const resolved = resolvePath(cwd, positional);
diff --git a/packages/cli/src/main.test.ts b/packages/cli/src/main.test.ts
index a0b38f9..4338822 100644
--- a/packages/cli/src/main.test.ts
+++ b/packages/cli/src/main.test.ts
@@ -163,6 +163,76 @@ test("buildAssemblyRoot wires every core service and registers the 'tecode' modu
}
});
+// --- buildAssemblyRoot's `configDir` deps (Req 9.6, Issue #81 Phase 1's
+// `--config ` flag) — the end-to-end proof that a `--config`
+// directory's `settings.json`/`keybindings.json` genuinely take effect,
+// not just that the string was threaded through unchanged. ---
+
+test("buildAssemblyRoot's configDir makes a --config directory's settings.json genuinely take effect", async () => {
+ const workspaceDir = await mkdtemp(join(tmpdir(), "tecode-cli-ws-"));
+ const configDir = await mkdtemp(join(tmpdir(), "tecode-cli-config-"));
+ await writeFile(
+ join(configDir, "settings.json"),
+ JSON.stringify({ "editor.tabSize": 2 }),
+ "utf8",
+ );
+
+ let root: ReturnType;
+ try {
+ root = buildAssemblyRoot(workspaceDir, { configDir });
+ await root.config.ready;
+
+ // The value actually came from configDir's settings.json, not from
+ // core's own default (4, `config/coreDefaults.ts`) — proof the
+ // override was genuinely read, not merely accepted and ignored.
+ expect(root.config.get("editor.tabSize")).toBe(2);
+ } finally {
+ root!.config.dispose();
+ root!.chordMachine.dispose();
+ root!.editorSession.dispose();
+ root!.editorLangIdSync.dispose();
+ root!.themeConfigSync.dispose();
+ root!.themeSelectCommand.dispose();
+ await rm(workspaceDir, { recursive: true, force: true });
+ await rm(configDir, { recursive: true, force: true });
+ }
+});
+
+test("buildAssemblyRoot's configDir makes a --config directory's keybindings.json genuinely take effect", async () => {
+ const workspaceDir = await mkdtemp(join(tmpdir(), "tecode-cli-ws-"));
+ const configDir = await mkdtemp(join(tmpdir(), "tecode-cli-config-"));
+ await writeFile(
+ join(configDir, "keybindings.json"),
+ JSON.stringify([{ key: "ctrl+alt+k", command: "fixture.fromConfigDir" }]),
+ "utf8",
+ );
+
+ let root: ReturnType;
+ try {
+ root = buildAssemblyRoot(workspaceDir, { configDir });
+ await root.config.ready;
+
+ expect(root.config.getKeybindingEntries()).toEqual([
+ { key: "ctrl+alt+k", command: "fixture.fromConfigDir" },
+ ]);
+ // buildAssemblyRoot wires onKeybindingsChange straight into
+ // keymap.setUserEntries — this proves the whole chain, not just
+ // ConfigService's own raw entry array.
+ const resolved = root.keymap.getTable().lookup("ctrl+alt+k", () => undefined);
+ expect(resolved?.command).toBe("fixture.fromConfigDir");
+ expect(resolved?.layer).toBe("user");
+ } finally {
+ root!.config.dispose();
+ root!.chordMachine.dispose();
+ root!.editorSession.dispose();
+ root!.editorLangIdSync.dispose();
+ root!.themeConfigSync.dispose();
+ root!.themeSelectCommand.dispose();
+ await rm(workspaceDir, { recursive: true, force: true });
+ await rm(configDir, { recursive: true, force: true });
+ }
+});
+
test("forward-referenced activateExtension is a safe no-op before the deferred phase assigns hostRef", async () => {
const dir = await mkdtemp(join(tmpdir(), "tecode-cli-root-"));
const savedHome = process.env["HOME"];
diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts
index 271fd66..dbd11af 100644
--- a/packages/cli/src/main.ts
+++ b/packages/cli/src/main.ts
@@ -81,7 +81,8 @@ import {
builtinManifests,
builtinThemeAssets,
} from "@tecode/builtin";
-import { resolveStartupTarget, type StartupTarget } from "./argv";
+import { join as joinPath } from "node:path";
+import { resolveConfigDirOverride, resolveStartupTarget, type StartupTarget } from "./argv";
import { buildExtensionDirMap, buildExtensionRecords } from "./extensionRecords";
import { createKeymapState, type KeymapState } from "./keymapState";
import { createBuiltinLanguageAssetsFs } from "./languageAssetsFs";
@@ -498,6 +499,17 @@ export function buildAssemblyRoot(
* hermeticity" shape.
*/
loadFallbackKeybindings?: () => Promise;
+ /**
+ * `--config `'s resolved value (Req 9.6, Issue #81 Phase 1) —
+ * `runTecode` threads `RunTecodeOptions.configDir` through to here.
+ * When set, `/settings.json` and `/keybindings.json`
+ * override `createConfigService`'s USER-layer defaults
+ * (`getUserSettingsPath()`/`getUserKeybindingsPath()`) — the
+ * workspace layer (`/.tecode/settings.json`) is
+ * entirely unaffected. `undefined` (the default) leaves both at their
+ * ordinary home-directory paths, exactly as before this flag existed.
+ */
+ configDir?: string;
} = {},
): AssemblyRoot {
const log = deps.log ?? createHostLog();
@@ -641,10 +653,25 @@ export function buildAssemblyRoot(
if (generation !== kittyVerdictGeneration) return;
keymap.setFallbackEntries(entries);
}
+ // `--config ` (Req 9.6, Issue #81 Phase 1): a caller-supplied
+ // directory overrides where the USER settings/keybindings layer is
+ // read from — `deps.configDir` derives both file paths from that ONE
+ // directory, matching the home-directory default's own "one directory,
+ // two well-known filenames" shape (`host/paths.ts`'s
+ // `getUserSettingsPath`/`getUserKeybindingsPath`). `undefined` when
+ // `deps.configDir` is unset, so `createConfigService` falls through to
+ // its own `getUserSettingsPath()`/`getUserKeybindingsPath()` defaults
+ // exactly as before this flag existed.
+ const settingsPath = deps.configDir ? joinPath(deps.configDir, "settings.json") : undefined;
+ const keybindingsPath = deps.configDir
+ ? joinPath(deps.configDir, "keybindings.json")
+ : undefined;
const config = createConfigService({
log,
sink,
workspaceRoot,
+ settingsPath,
+ keybindingsPath,
onKeybindingsChange: (entries) => keymap.setUserEntries(entries),
});
// Core's own settings (`editor.lineNumbers`, `editor.tabSize` — Req 9.5,
@@ -1072,6 +1099,12 @@ export interface RunTecodeOptions {
builtins?: Manifest[];
/** Overrides `process.cwd()` — tests only. */
cwd?: string;
+ /** `--config `'s resolved value (Req 9.6, Issue #81 Phase 1),
+ * already parsed by `main()`'s `resolveConfigDirOverride(argv)` call —
+ * threaded straight through to {@link buildAssemblyRoot}'s own
+ * `configDir` deps field (see that field's TSDoc for what it does).
+ * `undefined` (the default) is the ordinary "no override" case. */
+ configDir?: string;
}
/** Sets up graceful-shutdown handling (Phase 3's "wire process-exit
@@ -1197,7 +1230,7 @@ export async function runTecode(
// `renderShell`'s `onCapabilitiesResolved` callback, once the render
// seam has actually opened (or not opened, for `renderShellHeadless`) a
// real terminal.
- const root = buildAssemblyRoot(target.workspaceRoot, { log });
+ const root = buildAssemblyRoot(target.workspaceRoot, { log, configDir: options.configDir });
await root.config.ready;
emitVerboseStep(startedAt, "config-ready");
@@ -1348,12 +1381,22 @@ export async function runTecode(
return { root, extensionHost: deferred.extensionHost, loadResult: deferred.loadResult, firstFrameMs };
}
+/**
+ * The CLI entry point (Req 12.1, Issue #81 Phase 1's `--config `
+ * flag): handles `--version` first, exiting before any other argv
+ * handling ever runs (`argv.ts`'s top-of-file TSDoc: `resolveStartupTarget`
+ * must never see it either, for the same reason). `--config` is parsed
+ * right after, at that same early, synchronous, no-I/O position — via
+ * `resolveConfigDirOverride(argv)` — and its value is threaded through to
+ * {@link runTecode} as `RunTecodeOptions.configDir`.
+ */
async function main(argv: string[]): Promise {
if (argv.includes("--version")) {
console.log(pkg.version);
process.exit(0);
}
- await runTecode(argv);
+ const configDir = resolveConfigDirOverride(argv);
+ await runTecode(argv, { configDir });
}
// `import.meta.main` is Bun's "am I the entry point" check (true only when
diff --git a/packages/core/src/config/service.test.ts b/packages/core/src/config/service.test.ts
index be1b144..ae92ec4 100644
--- a/packages/core/src/config/service.test.ts
+++ b/packages/core/src/config/service.test.ts
@@ -155,6 +155,83 @@ describe("ConfigService.get — layering (Req 9.2, 9.3)", () => {
});
});
+describe("ConfigService — settingsPath/keybindingsPath overrides (Req 9.6, Issue #81 Phase 1)", () => {
+ test("settingsPath overrides where the user settings layer is read from, leaving the workspace layer untouched", async () => {
+ const overriddenSettingsPath = "/override/settings.json";
+ const workspaceRoot = "/fake-workspace";
+ const workspacePath = getWorkspaceSettingsPath(workspaceRoot);
+ // The ordinary default location is seeded too, to prove it is genuinely
+ // NOT read once settingsPath is supplied — a service that ignored the
+ // override and fell back to the default would report tabSize 99, not 2.
+ const defaultUserPath = getUserSettingsPath();
+ const fake = createFakeFs({
+ [overriddenSettingsPath]: JSON.stringify({ "editor.tabSize": 2 }),
+ [defaultUserPath]: JSON.stringify({ "editor.tabSize": 99 }),
+ [workspacePath]: JSON.stringify({ "editor.insertSpaces": false }),
+ });
+ const log = createHostLog();
+ const { sink } = createRecordingSink();
+ const service = createConfigService({
+ log,
+ sink,
+ workspaceRoot,
+ settingsPath: overriddenSettingsPath,
+ fs: fake.fs,
+ });
+ await service.ready;
+
+ expect(service.get("editor.tabSize")).toBe(2);
+ // The workspace layer is a completely separate path, resolved from
+ // workspaceRoot exactly as always — settingsPath overrides the USER
+ // layer only (this describe block's TSDoc).
+ expect(service.get("editor.insertSpaces")).toBe(false);
+ expect(fake.watchedPaths()).toContain(overriddenSettingsPath);
+ expect(fake.watchedPaths()).not.toContain(defaultUserPath);
+ service.dispose();
+ });
+
+ test("keybindingsPath overrides where the user keybindings layer is read from", async () => {
+ const overriddenKeybindingsPath = "/override/keybindings.json";
+ const defaultKeybindingsPath = getUserKeybindingsPath();
+ const entries = [{ key: "ctrl+k", command: "fixture.command" }];
+ const fake = createFakeFs({
+ [overriddenKeybindingsPath]: JSON.stringify(entries),
+ [defaultKeybindingsPath]: JSON.stringify([{ key: "ctrl+q", command: "should.not.load" }]),
+ });
+ const log = createHostLog();
+ const { sink } = createRecordingSink();
+ const service = createConfigService({
+ log,
+ sink,
+ keybindingsPath: overriddenKeybindingsPath,
+ fs: fake.fs,
+ });
+ await service.ready;
+
+ expect(service.getKeybindingEntries()).toEqual(entries);
+ expect(fake.watchedPaths()).toContain(overriddenKeybindingsPath);
+ expect(fake.watchedPaths()).not.toContain(defaultKeybindingsPath);
+ service.dispose();
+ });
+
+ test("with no settingsPath/keybindingsPath given, the ordinary home-directory defaults are used", async () => {
+ const defaultUserPath = getUserSettingsPath();
+ const defaultKeybindingsPath = getUserKeybindingsPath();
+ const fake = createFakeFs({
+ [defaultUserPath]: JSON.stringify({ "editor.tabSize": 7 }),
+ [defaultKeybindingsPath]: JSON.stringify([{ key: "a", command: "b" }]),
+ });
+ const log = createHostLog();
+ const { sink } = createRecordingSink();
+ const service = createConfigService({ log, sink, fs: fake.fs });
+ await service.ready;
+
+ expect(service.get("editor.tabSize")).toBe(7);
+ expect(service.getKeybindingEntries()).toEqual([{ key: "a", command: "b" }]);
+ service.dispose();
+ });
+});
+
describe("ConfigService.registerConfiguration (Req 9.3)", () => {
test("populates defaults for properties that declare one", async () => {
const fake = createFakeFs();
diff --git a/packages/core/src/config/service.ts b/packages/core/src/config/service.ts
index 4f775fe..f60eb12 100644
--- a/packages/core/src/config/service.ts
+++ b/packages/core/src/config/service.ts
@@ -9,6 +9,14 @@
* convention — matches `createCommandRegistry`, `createDocumentManager`,
* `createContextService`).
*
+ * **User-layer path overrides** (Req 9.6, Issue #81 Phase 1):
+ * {@link ConfigServiceDeps.settingsPath}/`keybindingsPath` let a caller
+ * (the CLI's `--config ` flag) redirect the USER `settings.json`/
+ * `keybindings.json` layer to a different directory, without touching how
+ * the workspace layer (`/.tecode/settings.json`) is
+ * resolved at all — same `deps.path ?? getUserXPath()` convention as
+ * `ui/themeSettingsWriter.ts`/`ui/keybindingsCommands.ts`.
+ *
* **Initialization design choice**: `createConfigService` returns
* synchronously (it does no I/O before returning), then kicks off the
* initial file reads and watcher setup in the background. Callers that need
@@ -100,6 +108,19 @@ export interface ConfigServiceDeps {
* this is provided — a single-file session with no workspace has no
* third layer. */
workspaceRoot?: string;
+ /** Overrides the user `settings.json` path (Req 9.6, Issue #81 Phase 1:
+ * the CLI's `--config ` flag) — tests use a temp file; production
+ * defaults to {@link getUserSettingsPath}. Matches
+ * `ui/themeSettingsWriter.ts`'s `ThemeSettingsWriterDeps.path`/
+ * `ui/keybindingsCommands.ts`'s own `deps.path ?? getUserXPath()`
+ * convention. Leaves the workspace settings layer
+ * (`/.tecode/settings.json`) completely untouched —
+ * `--config` overrides the USER layer only, never the workspace one. */
+ settingsPath?: string;
+ /** Overrides the user `keybindings.json` path (Req 9.6, Issue #81 Phase
+ * 1) — same override convention and same "user layer only" scope as
+ * {@link settingsPath}. Defaults to {@link getUserKeybindingsPath}. */
+ keybindingsPath?: string;
/** Filesystem seam — see {@link ConfigServiceFs}. Defaults to
* `node:fs/promises` + `node:fs.watch`. */
fs?: ConfigServiceFs;
@@ -229,11 +250,11 @@ export function createConfigService(deps: ConfigServiceDeps): ConfigService {
const { log, sink, workspaceRoot } = deps;
const fs = deps.fs ?? createNodeConfigFs();
- const userSettingsPath = getUserSettingsPath();
+ const userSettingsPath = deps.settingsPath ?? getUserSettingsPath();
const workspaceSettingsPath = workspaceRoot
? getWorkspaceSettingsPath(workspaceRoot)
: undefined;
- const keybindingsPath = getUserKeybindingsPath();
+ const keybindingsPath = deps.keybindingsPath ?? getUserKeybindingsPath();
const schemas = new Map();
const defaultsLayer: Record = {};
diff --git a/requirements.md b/requirements.md
index 34625cb..c3fbf93 100644
--- a/requirements.md
+++ b/requirements.md
@@ -147,6 +147,7 @@ The following points were open in the draft specification and are resolved here
3. Extensions SHALL declare their settings schema via `contributes.configuration`, and SHALL read values via `tecode.config.get(key)`.
4. WHEN a settings file is saved, THE system SHALL apply the changes immediately and fire `onDidChangeConfiguration`.
5. THE MVP settings SHALL include at least: `workbench.colorTheme`, `editor.tabSize`, `editor.insertSpaces`, `editor.wordWrap`, `editor.lineNumbers`, `explorer.showHidden`, and `files.autoSave`.
+6. WHEN tecode is launched with `--config `, THE system SHALL read the user settings and user keybindings layers from `/settings.json` and `/keybindings.json` instead of their home-directory defaults, leaving the workspace settings layer (`.tecode/settings.json`) unaffected.
### Requirement 10: Public Extension API