@bastani/atomic 0.5.28 → 0.5.29-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.
@@ -17,11 +17,6 @@
17
17
  "enum": ["github", "azure-devops", "sapling"],
18
18
  "description": "Selected source control provider. On atomic startup, the corresponding GitHub / Azure DevOps MCP servers are enabled in `.claude/settings.json` and `.opencode/opencode.json`; the others are disabled to avoid unnecessary token consumption."
19
19
  },
20
- "lastUpdated": {
21
- "type": "string",
22
- "format": "date-time",
23
- "description": "ISO 8601 timestamp of the last configuration update."
24
- },
25
20
  "providers": {
26
21
  "type": "object",
27
22
  "description": "Per-provider overrides for chatFlags and envVars. chatFlags replaces built-in defaults entirely when set; envVars are merged on top of defaults (user values win on conflict). Local .atomic/settings.json takes precedence over global ~/.atomic/settings.json.",
@@ -17,8 +17,6 @@ export declare function isScmProvider(value: unknown): value is ScmProvider;
17
17
  export interface AtomicConfig {
18
18
  /** Version of config schema */
19
19
  version?: number;
20
- /** Timestamp of last init */
21
- lastUpdated?: string;
22
20
  /** Selected source control provider (drives MCP server enable/disable sync). */
23
21
  scm?: ScmProvider;
24
22
  /** Per-provider overrides for chatFlags and envVars */
@@ -1 +1 @@
1
- {"version":3,"file":"atomic-config.d.ts","sourceRoot":"","sources":["../../../src/services/config/atomic-config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAOnE,0EAA0E;AAC1E,eAAO,MAAM,aAAa,gDAAiD,CAAC;AAC5E,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAElE;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,+BAA+B;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,GAAG,CAAC,EAAE,WAAW,CAAC;IAClB,uDAAuD;IACvD,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC;CAC1D;AA0HD;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAMvF;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,GAC7B,OAAO,CAAC,IAAI,CAAC,CAsBf;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,QAAQ,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,CAAC,CAG5B"}
1
+ {"version":3,"file":"atomic-config.d.ts","sourceRoot":"","sources":["../../../src/services/config/atomic-config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAOnE,0EAA0E;AAC1E,eAAO,MAAM,aAAa,gDAAiD,CAAC;AAC5E,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAElE;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,+BAA+B;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,GAAG,CAAC,EAAE,WAAW,CAAC;IAClB,uDAAuD;IACvD,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,QAAQ,EAAE,iBAAiB,CAAC,CAAC,CAAC;CAC1D;AAuHD;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAMvF;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,GAC7B,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,QAAQ,EAAE,QAAQ,EAClB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,CAAC,CAG5B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/atomic",
3
- "version": "0.5.28",
3
+ "version": "0.5.29-0",
4
4
  "description": "Configuration management CLI and SDK for coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -61,15 +61,12 @@
61
61
  "typecheck": "bunx tsc --noEmit && bunx tsc -p tests --noEmit",
62
62
  "lint": "oxlint --config=oxlint.json src",
63
63
  "lint:fix": "oxlint --config=oxlint.json --fix src",
64
- "prepare": "lefthook install || true"
64
+ "prepare": "prek install -t pre-commit -t pre-push || true"
65
65
  },
66
- "trustedDependencies": [
67
- "lefthook"
68
- ],
69
66
  "devDependencies": {
67
+ "@j178/prek": "^0.3.10",
70
68
  "@types/bun": "^1.3.12",
71
69
  "@types/react": "^19.2.14",
72
- "lefthook": "^2.1.6",
73
70
  "oxlint": "^1.61.0",
74
71
  "typescript": "^6.0.3",
75
72
  "typescript-language-server": "^5.1.3"
package/src/cli.ts CHANGED
@@ -404,6 +404,12 @@ export const program = createProgram();
404
404
  */
405
405
  async function main(): Promise<void> {
406
406
  try {
407
+ // Bootstrap `~/.atomic/settings.json` on every invocation if absent,
408
+ // so users always have a file to edit with JSON Schema intellisense
409
+ // wired up. Idempotent; swallows FS errors internally.
410
+ const { ensureGlobalAtomicSettings } = await import("./services/config/settings.ts");
411
+ await ensureGlobalAtomicSettings();
412
+
407
413
  // Sync tooling deps and global skills on first launch after install
408
414
  // or upgrade. Runs at most once per version bump (gated on a marker
409
415
  // file under ~/.atomic). Skipped for `--version` / `--help` so info
@@ -30,8 +30,6 @@ export function isScmProvider(value: unknown): value is ScmProvider {
30
30
  export interface AtomicConfig {
31
31
  /** Version of config schema */
32
32
  version?: number;
33
- /** Timestamp of last init */
34
- lastUpdated?: string;
35
33
  /** Selected source control provider (drives MCP server enable/disable sync). */
36
34
  scm?: ScmProvider;
37
35
  /** Per-provider overrides for chatFlags and envVars */
@@ -97,10 +95,8 @@ function pickAtomicConfig(record: JsonRecord | null): AtomicConfig | null {
97
95
 
98
96
  const config: AtomicConfig = {};
99
97
  const version = record.version;
100
- const lastUpdated = record.lastUpdated;
101
98
 
102
99
  if (typeof version === "number") config.version = version;
103
- if (typeof lastUpdated === "string") config.lastUpdated = lastUpdated;
104
100
  if (isScmProvider(record.scm)) config.scm = record.scm;
105
101
 
106
102
  const providers = pickProviders(record.providers);
@@ -144,7 +140,6 @@ function mergeConfigs(...configs: Array<AtomicConfig | null>): AtomicConfig | nu
144
140
  for (const config of configs) {
145
141
  if (!config) continue;
146
142
  if (config.version !== undefined) merged.version = config.version;
147
- if (config.lastUpdated !== undefined) merged.lastUpdated = config.lastUpdated;
148
143
  if (config.scm !== undefined) merged.scm = config.scm;
149
144
 
150
145
  if (config.providers) {
@@ -186,7 +181,6 @@ export async function saveAtomicConfig(
186
181
  ...currentConfig,
187
182
  ...updates,
188
183
  version: 1,
189
- lastUpdated: new Date().toISOString(),
190
184
  };
191
185
 
192
186
  const nextSettings: JsonRecord = {
@@ -20,7 +20,6 @@ import type { ScmProvider } from "./atomic-config.ts";
20
20
  interface AtomicSettings {
21
21
  $schema?: string;
22
22
  version?: number;
23
- lastUpdated?: string;
24
23
  telemetryEnabled?: boolean;
25
24
  scm?: ScmProvider;
26
25
  providers?: Partial<Record<AgentKey, ProviderOverrides>>;
@@ -54,6 +53,25 @@ async function writeGlobalSettings(settings: AtomicSettings): Promise<void> {
54
53
  await Bun.write(path, JSON.stringify(settings, null, 2));
55
54
  }
56
55
 
56
+ /**
57
+ * Ensure `~/.atomic/settings.json` exists. Called once at CLI startup so
58
+ * users have a valid file to edit (with JSON Schema intellisense wired up
59
+ * via `$schema`) without having to run any explicit init command.
60
+ *
61
+ * Idempotent — no-op if the file already exists. Best-effort: filesystem
62
+ * errors (e.g. read-only home) are swallowed so the CLI never blocks on
63
+ * this side-effect.
64
+ */
65
+ export async function ensureGlobalAtomicSettings(): Promise<void> {
66
+ try {
67
+ const path = globalSettingsPath();
68
+ if (await Bun.file(path).exists()) return;
69
+ await writeGlobalSettings({ version: 1 });
70
+ } catch (e) {
71
+ console.warn(`[settings] failed to bootstrap global settings: ${errorMessage(e)}`);
72
+ }
73
+ }
74
+
57
75
  /**
58
76
  * Set telemetry enabled/disabled in global settings.
59
77
  */
@@ -13,8 +13,13 @@ import {
13
13
  test,
14
14
  expect,
15
15
  beforeEach,
16
+ beforeAll,
17
+ afterAll,
16
18
  mock,
17
19
  } from "bun:test";
20
+ import { chmodSync, mkdtempSync, writeFileSync } from "node:fs";
21
+ import { tmpdir } from "node:os";
22
+ import { join } from "node:path";
18
23
 
19
24
  // ─── Copilot SDK fake ──────────────────────────────────────────────────────
20
25
  // `CopilotClient` is a class; the constructor captures latest test state.
@@ -45,10 +50,6 @@ class FakeCopilotClient {
45
50
  }
46
51
  }
47
52
 
48
- mock.module("@github/copilot-sdk", () => ({
49
- CopilotClient: FakeCopilotClient,
50
- }));
51
-
52
53
  // ─── Claude Agent SDK fake ────────────────────────────────────────────────
53
54
  // `query()` returns something with `initializationResult()` and `close()`.
54
55
  // We ignore the `prompt` stream — the real SDK consumes it lazily, and the
@@ -65,17 +66,50 @@ let claudeInit = mock<() => Promise<{ account: ClaudeAccount }>>(async () => ({
65
66
  }));
66
67
  let claudeClose = mock(() => {});
67
68
 
68
- mock.module("@anthropic-ai/claude-agent-sdk", () => ({
69
- query: () => ({
70
- initializationResult: () => claudeInit(),
71
- close: () => claudeClose(),
72
- }),
73
- }));
69
+ // `mock.module` is process-global in Bun and leaks across every test file
70
+ // loaded in the same run — live ESM bindings in other files rebind to the
71
+ // stub as soon as it registers, and re-registering with the real module in
72
+ // `afterAll` does not restore the original namespace identity. Capture the
73
+ // real SDK modules first, install the mocks only while this file's tests
74
+ // are running, and never mock `claude.ts` (other test files exercise its
75
+ // real exports). All consumers in `auth.ts` use dynamic `await import(...)`,
76
+ // so lazy mock registration is safe here.
77
+ const actualCopilotSdk = await import("@github/copilot-sdk");
78
+ const actualClaudeSdk = await import("@anthropic-ai/claude-agent-sdk");
79
+
80
+ // Put a fake `claude` binary on PATH so `resolveHeadlessClaudeBin()` (called
81
+ // by `checkClaudeAuth`) succeeds without hitting the real CLI on disk. The
82
+ // mocked SDK `query()` never actually spawns the subprocess — the path is
83
+ // only passed through to the SDK constructor.
84
+ let pathBefore: string | undefined;
85
+
86
+ beforeAll(() => {
87
+ const dir = mkdtempSync(join(tmpdir(), "atomic-auth-test-path-"));
88
+ const bin = join(dir, "claude");
89
+ writeFileSync(bin, "#!/usr/bin/env sh\nexit 0\n");
90
+ chmodSync(bin, 0o755);
91
+ pathBefore = process.env.PATH;
92
+ process.env.PATH = `${dir}:${process.env.PATH ?? ""}`;
93
+
94
+ mock.module("@github/copilot-sdk", () => ({
95
+ ...actualCopilotSdk,
96
+ CopilotClient: FakeCopilotClient,
97
+ }));
98
+ mock.module("@anthropic-ai/claude-agent-sdk", () => ({
99
+ ...actualClaudeSdk,
100
+ query: () => ({
101
+ initializationResult: () => claudeInit(),
102
+ close: () => claudeClose(),
103
+ }),
104
+ }));
105
+ });
74
106
 
75
- // Stub the claude provider module so we don't probe PATH for `claude`.
76
- mock.module("../../sdk/providers/claude.ts", () => ({
77
- resolveHeadlessClaudeBin: () => "/usr/local/bin/claude",
78
- }));
107
+ afterAll(() => {
108
+ if (pathBefore === undefined) delete process.env.PATH;
109
+ else process.env.PATH = pathBefore;
110
+ mock.module("@github/copilot-sdk", () => ({ ...actualCopilotSdk }));
111
+ mock.module("@anthropic-ai/claude-agent-sdk", () => ({ ...actualClaudeSdk }));
112
+ });
79
113
 
80
114
  const { checkAgentAuth } = await import("./auth.ts");
81
115