@neocompose/cli 0.26.5 → 0.27.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/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.27.0] - 2026-08-10
4
+
5
+ ### Added
6
+
7
+ - `neo logout [--api <url>]`: targeted local credential deletion for exactly
8
+ one API origin — the namespaced OS-keychain entry and the credentials-file
9
+ row — plus best-effort server session revocation. Other origins, other
10
+ namespaces, and the rest of the config home are never touched.
11
+ - `NEO_COMPOSE_CONFIG_HOME`: an absolute directory that replaces
12
+ `$XDG_CONFIG_HOME/neo-compose` for Neo credential state only, so isolated
13
+ environments (P53 agent rigs) keep their own credential store without
14
+ changing the ambient XDG configuration of unrelated tools.
15
+ - `NEO_COMPOSE_CREDENTIAL_NAMESPACE`: scopes the OS-keychain account
16
+ (`<namespace>::<api origin>`) so isolated environments sharing one OS user
17
+ cannot read or delete each other's tokens.
18
+
19
+ ### Fixed
20
+
21
+ - The credential-store docstring described a two-level precedence with the
22
+ OS keychain "layering in later"; the keychain layer has existed for some
23
+ time. The docstring now matches the real precedence (env var, then
24
+ keychain, then credentials file) and documents that the metadata row is
25
+ written even when the keychain holds the token.
26
+
3
27
  ## [0.26.5] - 2026-08-10
4
28
 
5
29
  ### Changed
package/README.md CHANGED
@@ -419,6 +419,7 @@ authorization boundary.
419
419
  | `--version` | Print the installed CLI package version. |
420
420
  | `login` | Authenticate the selected profile. |
421
421
  | `whoami` | Inspect the selected profile. |
422
+ | `logout [--api <url>]` | Delete the stored credential for one API origin and revoke its session. |
422
423
  | `init --project <id> [--version <id>]` | Create a format-4 working copy and perform its first reset pull. |
423
424
  | `doctor` | Validate format/compiler/editor/source/file contracts; no .NET discovery. |
424
425
  | `pull [--force\|--reset] [--regenerate-source-names]` | Merge remote changes or regenerate canonical source, names, and binaries. |
package/dist/neo.mjs CHANGED
@@ -44,7 +44,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
44
44
  // src/token-store.ts
45
45
  var token_store_exports = {};
46
46
  __export(token_store_exports, {
47
+ NEO_CONFIG_HOME_ENV_VAR: () => NEO_CONFIG_HOME_ENV_VAR,
48
+ NEO_CREDENTIAL_NAMESPACE_ENV_VAR: () => NEO_CREDENTIAL_NAMESPACE_ENV_VAR,
47
49
  NEO_TOKEN_ENV_VAR: () => NEO_TOKEN_ENV_VAR,
50
+ credentialsDir: () => credentialsDir,
51
+ deleteCredential: () => deleteCredential,
52
+ keychainAccount: () => keychainAccount,
48
53
  loadCredential: () => loadCredential,
49
54
  loadToken: () => loadToken,
50
55
  saveCredential: () => saveCredential
@@ -54,12 +59,22 @@ import {
54
59
  existsSync,
55
60
  mkdirSync,
56
61
  readFileSync,
62
+ rmSync,
57
63
  writeFileSync
58
64
  } from "node:fs";
59
65
  import { execFileSync } from "node:child_process";
60
66
  import { homedir } from "node:os";
61
- import { join } from "node:path";
67
+ import { isAbsolute, join } from "node:path";
62
68
  function credentialsDir() {
69
+ const configHome = process.env[NEO_CONFIG_HOME_ENV_VAR];
70
+ if (configHome !== void 0 && configHome !== "") {
71
+ if (!isAbsolute(configHome)) {
72
+ throw new Error(
73
+ `${NEO_CONFIG_HOME_ENV_VAR} must be an absolute directory path, got "${configHome}".`
74
+ );
75
+ }
76
+ return configHome;
77
+ }
63
78
  const xdg = process.env.XDG_CONFIG_HOME;
64
79
  const base = xdg !== void 0 && xdg !== "" ? xdg : join(homedir(), ".config");
65
80
  return join(base, "neo-compose");
@@ -80,6 +95,11 @@ function readCredentialsFile() {
80
95
  }
81
96
  return file;
82
97
  }
98
+ function keychainAccount(apiBaseUrl) {
99
+ const namespace = process.env[NEO_CREDENTIAL_NAMESPACE_ENV_VAR];
100
+ if (namespace === void 0 || namespace === "") return apiBaseUrl;
101
+ return `${namespace}::${apiBaseUrl}`;
102
+ }
83
103
  function keychainSet(account, secret) {
84
104
  if (process.platform !== "darwin") return false;
85
105
  try {
@@ -116,8 +136,24 @@ function keychainGet(account) {
116
136
  return null;
117
137
  }
118
138
  }
139
+ function keychainDelete(account) {
140
+ if (process.platform !== "darwin") return false;
141
+ try {
142
+ execFileSync(
143
+ "security",
144
+ ["delete-generic-password", "-s", KEYCHAIN_SERVICE, "-a", account],
145
+ { stdio: "ignore" }
146
+ );
147
+ return true;
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
119
152
  function saveCredential(credential) {
120
- const inKeychain = keychainSet(credential.apiBaseUrl, credential.token);
153
+ const inKeychain = keychainSet(
154
+ keychainAccount(credential.apiBaseUrl),
155
+ credential.token
156
+ );
121
157
  mkdirSync(credentialsDir(), { recursive: true, mode: 448 });
122
158
  const file = readCredentialsFile();
123
159
  file.credentials[credential.apiBaseUrl] = inKeychain ? { ...credential, token: "" } : credential;
@@ -132,7 +168,7 @@ function saveCredential(credential) {
132
168
  function loadToken(apiBaseUrl) {
133
169
  const envToken = process.env[NEO_TOKEN_ENV_VAR];
134
170
  if (envToken !== void 0 && envToken !== "") return envToken;
135
- const fromKeychain = keychainGet(apiBaseUrl);
171
+ const fromKeychain = keychainGet(keychainAccount(apiBaseUrl));
136
172
  if (fromKeychain !== null) return fromKeychain;
137
173
  const file = readCredentialsFile();
138
174
  const credential = file.credentials[apiBaseUrl];
@@ -143,11 +179,38 @@ function loadCredential(apiBaseUrl) {
143
179
  const file = readCredentialsFile();
144
180
  return file.credentials[apiBaseUrl] ?? null;
145
181
  }
146
- var NEO_TOKEN_ENV_VAR, KEYCHAIN_SERVICE;
182
+ function deleteCredential(apiBaseUrl) {
183
+ const account = keychainAccount(apiBaseUrl);
184
+ const fromKeychain = keychainGet(account);
185
+ const file = readCredentialsFile();
186
+ const fileEntry = file.credentials[apiBaseUrl];
187
+ const token = fromKeychain ?? (fileEntry !== void 0 && fileEntry.token !== "" ? fileEntry.token : null);
188
+ const deletedKeychainEntry = keychainDelete(account);
189
+ let deletedFileEntry = false;
190
+ if (fileEntry !== void 0) {
191
+ delete file.credentials[apiBaseUrl];
192
+ deletedFileEntry = true;
193
+ const path = credentialsPath();
194
+ if (Object.keys(file.credentials).length === 0) {
195
+ rmSync(path, { force: true });
196
+ } else {
197
+ writeFileSync(path, `${JSON.stringify(file, null, 2)}
198
+ `, {
199
+ encoding: "utf8",
200
+ mode: 384
201
+ });
202
+ chmodSync(path, 384);
203
+ }
204
+ }
205
+ return { token, deletedKeychainEntry, deletedFileEntry };
206
+ }
207
+ var NEO_TOKEN_ENV_VAR, NEO_CONFIG_HOME_ENV_VAR, NEO_CREDENTIAL_NAMESPACE_ENV_VAR, KEYCHAIN_SERVICE;
147
208
  var init_token_store = __esm({
148
209
  "src/token-store.ts"() {
149
210
  "use strict";
150
211
  NEO_TOKEN_ENV_VAR = "NEO_COMPOSE_TOKEN";
212
+ NEO_CONFIG_HOME_ENV_VAR = "NEO_COMPOSE_CONFIG_HOME";
213
+ NEO_CREDENTIAL_NAMESPACE_ENV_VAR = "NEO_COMPOSE_CREDENTIAL_NAMESPACE";
151
214
  KEYCHAIN_SERVICE = "neo-compose-cli";
152
215
  }
153
216
  });
@@ -427,6 +490,44 @@ async function runLogin(options) {
427
490
  return;
428
491
  }
429
492
  }
493
+ async function runLogout(apiBaseUrl) {
494
+ const { deleteCredential: deleteCredential2 } = await Promise.resolve().then(() => (init_token_store(), token_store_exports));
495
+ const deleted = deleteCredential2(apiBaseUrl);
496
+ if (deleted.token === null && !deleted.deletedKeychainEntry && !deleted.deletedFileEntry) {
497
+ console.log(`No credentials stored for "${apiBaseUrl}".`);
498
+ return;
499
+ }
500
+ if (deleted.token !== null) {
501
+ try {
502
+ const response = await fetch(new URL("/api/auth/sign-out", apiBaseUrl), {
503
+ body: "{}",
504
+ headers: {
505
+ Authorization: `Bearer ${deleted.token}`,
506
+ "Content-Type": "application/json"
507
+ },
508
+ method: "POST"
509
+ });
510
+ if (response.ok) {
511
+ console.log(`Revoked the server session at ${apiBaseUrl}.`);
512
+ } else {
513
+ console.log(
514
+ `Server session revocation returned ${response.status}; the local credential was still deleted.`
515
+ );
516
+ }
517
+ } catch {
518
+ console.log(
519
+ "Server session revocation was unreachable; the local credential was still deleted."
520
+ );
521
+ }
522
+ }
523
+ const surfaces = [
524
+ deleted.deletedKeychainEntry ? "Keychain entry" : null,
525
+ deleted.deletedFileEntry ? "credentials file entry" : null
526
+ ].filter((surface) => surface !== null);
527
+ console.log(
528
+ `Logged out of ${apiBaseUrl}${surfaces.length > 0 ? ` (removed ${surfaces.join(" and ")})` : ""}.`
529
+ );
530
+ }
430
531
  async function runWhoami(apiBaseUrl) {
431
532
  const { loadToken: loadToken2 } = await Promise.resolve().then(() => (init_token_store(), token_store_exports));
432
533
  const token = loadToken2(apiBaseUrl);
@@ -93879,7 +93980,7 @@ import {
93879
93980
  readFileSync as readFileSync6,
93880
93981
  readdirSync,
93881
93982
  renameSync as renameSync4,
93882
- rmSync,
93983
+ rmSync as rmSync2,
93883
93984
  writeFileSync as writeFileSync6
93884
93985
  } from "node:fs";
93885
93986
  import { basename, dirname as dirname5, extname, join as join6, relative, sep } from "node:path";
@@ -94159,7 +94260,7 @@ function writeVerifiedBinaryDownloadV4(destination, bytes, expectedSha256) {
94159
94260
  writeFileSync6(temporary, bytes);
94160
94261
  renameSync4(temporary, destination);
94161
94262
  } finally {
94162
- rmSync(temporary, { force: true });
94263
+ rmSync2(temporary, { force: true });
94163
94264
  }
94164
94265
  }
94165
94266
  function writeBinaryConflictArtifactV4(root, fileId, fileName2, bytes, expectedSha256) {
@@ -95284,7 +95385,7 @@ import {
95284
95385
  mkdirSync as mkdirSync7,
95285
95386
  readFileSync as readFileSync8,
95286
95387
  readdirSync as readdirSync3,
95287
- rmSync as rmSync2,
95388
+ rmSync as rmSync3,
95288
95389
  writeFileSync as writeFileSync7,
95289
95390
  statSync
95290
95391
  } from "node:fs";
@@ -95296,14 +95397,14 @@ function resetWorkspaceToProjectSourcesV4(workspace, document, options = {}) {
95296
95397
  const previous = managedFilesBeforeReset(workspace.root);
95297
95398
  const preservedSpecs = preserveManagedSpecs(workspace.root);
95298
95399
  for (const directory of FORMAT_4_MANAGED_DIRECTORIES) {
95299
- rmSync2(join8(workspace.root, directory), { recursive: true, force: true });
95400
+ rmSync3(join8(workspace.root, directory), { recursive: true, force: true });
95300
95401
  }
95301
- rmSync2(join8(workspace.root, "Scripts"), { recursive: true, force: true });
95402
+ rmSync3(join8(workspace.root, "Scripts"), { recursive: true, force: true });
95302
95403
  for (const file of LEGACY_ROOT_FILES) {
95303
- rmSync2(join8(workspace.root, file), { force: true });
95404
+ rmSync3(join8(workspace.root, file), { force: true });
95304
95405
  }
95305
95406
  for (const privatePath of LEGACY_PRIVATE_PATHS) {
95306
- rmSync2(join8(workspace.root, privatePath), { recursive: true, force: true });
95407
+ rmSync3(join8(workspace.root, privatePath), { recursive: true, force: true });
95307
95408
  }
95308
95409
  for (const [path, bytes] of preservedSpecs) {
95309
95410
  const absolute = join8(workspace.root, path);
@@ -95559,7 +95660,7 @@ var init_http = __esm({
95559
95660
  });
95560
95661
 
95561
95662
  // src/project-source/project-file-pull.ts
95562
- import { existsSync as existsSync6, rmSync as rmSync3 } from "node:fs";
95663
+ import { existsSync as existsSync6, rmSync as rmSync4 } from "node:fs";
95563
95664
  import { join as join9 } from "node:path";
95564
95665
  async function pullProjectBinariesV4(args) {
95565
95666
  let client = args.client ?? null;
@@ -95688,7 +95789,7 @@ async function pullProjectBinariesV4(args) {
95688
95789
  conflicted += 1;
95689
95790
  continue;
95690
95791
  }
95691
- rmSync3(absolute, { force: true });
95792
+ rmSync4(absolute, { force: true });
95692
95793
  removePreviousConflict(args.workspace.root, previous.projectBinary);
95693
95794
  }
95694
95795
  return { states, downloaded, conflicted, conflicts };
@@ -95758,7 +95859,7 @@ function fileName(data) {
95758
95859
  }
95759
95860
  function removePreviousConflict(root, state) {
95760
95861
  if (state?.conflict?.artifactPath === void 0) return;
95761
- rmSync3(join9(root, state.conflict.artifactPath), { force: true });
95862
+ rmSync4(join9(root, state.conflict.artifactPath), { force: true });
95762
95863
  }
95763
95864
  var init_project_file_pull = __esm({
95764
95865
  "src/project-source/project-file-pull.ts"() {
@@ -95823,7 +95924,7 @@ __export(pull_exports, {
95823
95924
  import {
95824
95925
  mkdirSync as mkdirSync8,
95825
95926
  writeFileSync as writeFileSync8,
95826
- rmSync as rmSync4,
95927
+ rmSync as rmSync5,
95827
95928
  existsSync as existsSync7,
95828
95929
  readFileSync as readFileSync9
95829
95930
  } from "node:fs";
@@ -96212,7 +96313,7 @@ async function finishFormat4Pull(args) {
96212
96313
  if (previousPath === void 0 || emittedPaths.has(previousPath)) continue;
96213
96314
  const absolute = join10(workspace.root, previousPath);
96214
96315
  if (existsSync7(absolute)) {
96215
- rmSync4(absolute);
96316
+ rmSync5(absolute);
96216
96317
  removed += 1;
96217
96318
  }
96218
96319
  }
@@ -100378,7 +100479,7 @@ import { createHash as createHash10, randomUUID as randomUUID2 } from "node:cryp
100378
100479
  import {
100379
100480
  mkdirSync as mkdirSync10,
100380
100481
  writeFileSync as writeFileSync10,
100381
- rmSync as rmSync5,
100482
+ rmSync as rmSync6,
100382
100483
  existsSync as existsSync11,
100383
100484
  readFileSync as readFileSync15
100384
100485
  } from "node:fs";
@@ -101174,7 +101275,7 @@ async function runPush(workspace, options, preparationOverride) {
101174
101275
  });
101175
101276
  } finally {
101176
101277
  if (preparedBuildDir !== void 0) {
101177
- rmSync5(preparedBuildDir, { recursive: true, force: true });
101278
+ rmSync6(preparedBuildDir, { recursive: true, force: true });
101178
101279
  }
101179
101280
  }
101180
101281
  } else {
@@ -102387,7 +102488,7 @@ ${finalErrors.map(
102387
102488
  const previousPath = recordState.file;
102388
102489
  if (previousPath === void 0 || emittedPaths.has(previousPath)) continue;
102389
102490
  const absolute = join15(workspace.root, previousPath);
102390
- if (existsSync11(absolute)) rmSync5(absolute);
102491
+ if (existsSync11(absolute)) rmSync6(absolute);
102391
102492
  }
102392
102493
  for (const file of files) {
102393
102494
  const absolute = join15(workspace.root, file.path);
@@ -103197,7 +103298,7 @@ var init_registry2 = __esm({
103197
103298
  PROJECT_SCHEMA_CONTRACT = Object.freeze({
103198
103299
  formatVersion: 3,
103199
103300
  contractVersion: "3.9",
103200
- cliVersion: "0.26.5",
103301
+ cliVersion: "0.27.0",
103201
103302
  projectFileUploadBatchSize: 32,
103202
103303
  documentRecords: {
103203
103304
  member: {
@@ -104498,14 +104599,14 @@ import {
104498
104599
  readFileSync as readFileSync16,
104499
104600
  realpathSync,
104500
104601
  renameSync as renameSync5,
104501
- rmSync as rmSync6,
104602
+ rmSync as rmSync7,
104502
104603
  statSync as statSync2,
104503
104604
  writeFileSync as writeFileSync11
104504
104605
  } from "node:fs";
104505
104606
  import {
104506
104607
  basename as basename3,
104507
104608
  dirname as dirname9,
104508
- isAbsolute,
104609
+ isAbsolute as isAbsolute2,
104509
104610
  join as join16,
104510
104611
  relative as relative5,
104511
104612
  resolve as resolve3,
@@ -104706,7 +104807,7 @@ function preparedHookCandidate(workspace) {
104706
104807
  "test-build"
104707
104808
  );
104708
104809
  const pathFromRoot = relative5(cacheRoot, directory);
104709
- if (isAbsolute(pathFromRoot)) {
104810
+ if (isAbsolute2(pathFromRoot)) {
104710
104811
  throw new NeoTestPreparedCandidateError(
104711
104812
  "NEO_PREPARED_BUILD_DIR must not resolve to an absolute path outside this workspace's .neo/test-build directory."
104712
104813
  );
@@ -104906,7 +105007,7 @@ function selectedSpecPaths(workspace, selectors) {
104906
105007
  );
104907
105008
  for (const selector of normalizedSelectors) {
104908
105009
  if (/[*?]/u.test(selector)) continue;
104909
- if (isAbsolute(selector)) {
105010
+ if (isAbsolute2(selector)) {
104910
105011
  throw new Error(
104911
105012
  `Spec selector ${JSON.stringify(selector)} must be workspace-relative.`
104912
105013
  );
@@ -105691,16 +105792,16 @@ function maintainNeoTestBuildCache(root, maxBytes = TEST_BUILD_CACHE_LIMIT_BYTES
105691
105792
  const resolvedProtected = protectedDirectory === void 0 ? null : resolve3(protectedDirectory);
105692
105793
  const protectedInsideRoot = resolvedProtected !== null && (() => {
105693
105794
  const fromRoot = relative5(resolvedRoot, resolvedProtected);
105694
- return fromRoot === "" || !fromRoot.startsWith(`..${sep5}`) && fromRoot !== ".." && !isAbsolute(fromRoot);
105795
+ return fromRoot === "" || !fromRoot.startsWith(`..${sep5}`) && fromRoot !== ".." && !isAbsolute2(fromRoot);
105695
105796
  })();
105696
105797
  const isProtected = (path) => {
105697
105798
  if (!protectedInsideRoot || resolvedProtected === null) return false;
105698
105799
  const fromProtected = relative5(resolvedProtected, resolve3(path));
105699
- return fromProtected === "" || !fromProtected.startsWith(`..${sep5}`) && fromProtected !== ".." && !isAbsolute(fromProtected);
105800
+ return fromProtected === "" || !fromProtected.startsWith(`..${sep5}`) && fromProtected !== ".." && !isAbsolute2(fromProtected);
105700
105801
  };
105701
105802
  for (const file of testBuildFiles(root)) {
105702
105803
  if (!isProtected(file.path) && basename3(file.path).includes(".tmp-") && now - file.modifiedMs >= ABANDONED_TEMP_MAX_AGE_MS) {
105703
- rmSync6(file.path, { force: true });
105804
+ rmSync7(file.path, { force: true });
105704
105805
  }
105705
105806
  }
105706
105807
  const files = testBuildFiles(root);
@@ -105710,7 +105811,7 @@ function maintainNeoTestBuildCache(root, maxBytes = TEST_BUILD_CACHE_LIMIT_BYTES
105710
105811
  ).sort((left, right) => left.modifiedMs - right.modifiedMs);
105711
105812
  for (const file of removable) {
105712
105813
  if (total <= maxBytes) break;
105713
- rmSync6(file.path, { force: true });
105814
+ rmSync7(file.path, { force: true });
105714
105815
  total -= file.size;
105715
105816
  }
105716
105817
  }
@@ -106004,7 +106105,7 @@ async function runTest(workspace, options, dependencies = {}) {
106004
106105
  if (options.outputFile !== null) {
106005
106106
  try {
106006
106107
  atomicWrite(
106007
- isAbsolute(options.outputFile) ? options.outputFile : join16(workspace.root, options.outputFile),
106108
+ isAbsolute2(options.outputFile) ? options.outputFile : join16(workspace.root, options.outputFile),
106008
106109
  serialized
106009
106110
  );
106010
106111
  } catch (error) {
@@ -106217,7 +106318,7 @@ import {
106217
106318
  existsSync as existsSync13,
106218
106319
  readFileSync as readFileSync17
106219
106320
  } from "node:fs";
106220
- import { extname as extname2, isAbsolute as isAbsolute2, join as join17, relative as relative6, sep as sep6 } from "node:path";
106321
+ import { extname as extname2, isAbsolute as isAbsolute3, join as join17, relative as relative6, sep as sep6 } from "node:path";
106221
106322
  function inspectNeoDoctor(workspace) {
106222
106323
  const formatCompatible = workspace.config.formatVersion === CURRENT_FORMAT_VERSION;
106223
106324
  const compiler = inspectCompilerContract();
@@ -106397,7 +106498,7 @@ function inspectTrackedBinary(root, record3, errors) {
106397
106498
  const binary = record3.projectBinary;
106398
106499
  if (!binary) return;
106399
106500
  const path = binary.path.replaceAll("\\", "/");
106400
- if (isAbsolute2(path) || path.split("/").includes("..")) {
106501
+ if (isAbsolute3(path) || path.split("/").includes("..")) {
106401
106502
  errors.push(
106402
106503
  `Project file ${record3.recordId} has unsafe tracked path ${JSON.stringify(binary.path)}.`
106403
106504
  );
@@ -109474,7 +109575,7 @@ __export(resolve_exports, {
109474
109575
  runResolve: () => runResolve,
109475
109576
  workspaceFilePath: () => workspaceFilePath
109476
109577
  });
109477
- import { readFileSync as readFileSync19, rmSync as rmSync7, writeFileSync as writeFileSync14 } from "node:fs";
109578
+ import { readFileSync as readFileSync19, rmSync as rmSync8, writeFileSync as writeFileSync14 } from "node:fs";
109478
109579
  import { join as join21 } from "node:path";
109479
109580
  function runResolve(workspace, side) {
109480
109581
  let resolvedFiles = 0;
@@ -109500,12 +109601,12 @@ function runResolve(workspace, side) {
109500
109601
  );
109501
109602
  binary.sha256 = conflict2.remoteSha256;
109502
109603
  } else {
109503
- rmSync7(destination, { force: true });
109604
+ rmSync8(destination, { force: true });
109504
109605
  binary.sha256 = null;
109505
109606
  }
109506
109607
  }
109507
109608
  if (conflict2.artifactPath !== void 0) {
109508
- rmSync7(join21(workspace.root, conflict2.artifactPath), { force: true });
109609
+ rmSync8(join21(workspace.root, conflict2.artifactPath), { force: true });
109509
109610
  }
109510
109611
  delete binary.conflict;
109511
109612
  resolvedBinaries += 1;
@@ -109604,6 +109705,7 @@ ${h("Start")}
109604
109705
  login ${d("[--api <url>] [--profile editor|release] [--save-project <id>]")}
109605
109706
  init ${d("[--project <id>] [--version <id>] [--dir <path>] (interactive pickers)")}
109606
109707
  whoami ${d("[--api <url>]")}
109708
+ logout ${d("[--api <url>] delete the stored credential for one API origin")}
109607
109709
  help ${d("show this command overview")}
109608
109710
  --version ${d("print the installed CLI version")}
109609
109711
 
@@ -109762,7 +109864,7 @@ There is no --keep-current: preserving current semantic state defines flatten.
109762
109864
  async function main() {
109763
109865
  const args = parseArgs(process.argv.slice(2));
109764
109866
  if (args.command === "--version") {
109765
- console.log("0.26.5");
109867
+ console.log("0.27.0");
109766
109868
  return;
109767
109869
  }
109768
109870
  if (args.command === null || args.command === "help" || args.command === "--help" || args.command === "-h") {
@@ -109787,6 +109889,9 @@ async function main() {
109787
109889
  case "whoami":
109788
109890
  await runWhoami(apiBaseUrl);
109789
109891
  return;
109892
+ case "logout":
109893
+ await runLogout(apiBaseUrl);
109894
+ return;
109790
109895
  case "init":
109791
109896
  {
109792
109897
  const { runInit: runInit2 } = await Promise.resolve().then(() => (init_init(), init_exports));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neocompose/cli",
3
- "version": "0.26.5",
3
+ "version": "0.27.0",
4
4
  "description": "Neo Compose native project-source CLI with bidirectional sync.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,7 +9,7 @@ description: >-
9
9
  `@neocompose/cli` or `node cli/bin/neo.mjs` in the neo-compose repository.
10
10
  ---
11
11
 
12
- <!-- reviewed-through-cli: 0.26.5 -->
12
+ <!-- reviewed-through-cli: 0.27.0 -->
13
13
 
14
14
  # Neo Compose CLI
15
15
 
@@ -83,7 +83,7 @@ wrappers.
83
83
  The marker near the top of `SKILL.md` must exactly match the package version:
84
84
 
85
85
  ```html
86
- <!-- reviewed-through-cli: 0.26.5 -->
86
+ <!-- reviewed-through-cli: 0.27.0 -->
87
87
  ```
88
88
 
89
89
  The quoted version above is checked too, so this instruction cannot go stale
@@ -145,5 +145,16 @@ and a protected file only as fallback. Use `NEO_COMPOSE_TOKEN` or
145
145
  `--token-stdin` in CI. The editor profile cannot publish releases; server
146
146
  scopes remain the security boundary.
147
147
 
148
+ `neo logout [--api <url>]` deletes the stored credential for exactly one API
149
+ origin (keychain entry and credentials-file row) and revokes the server
150
+ session best-effort. It never clears other origins or the whole store.
151
+
152
+ Isolated environments (P53 agent rigs) set `NEO_COMPOSE_CONFIG_HOME` (an
153
+ absolute directory replacing `$XDG_CONFIG_HOME/neo-compose` for Neo state
154
+ only) and `NEO_COMPOSE_CREDENTIAL_NAMESPACE` (scopes the OS-keychain account)
155
+ so concurrent environments sharing one OS user cannot read, overwrite, or
156
+ delete each other's tokens. Login, logout, and authenticated commands all
157
+ honor both variables.
158
+
148
159
  Pass explicit project/version IDs and flags in automation. Prefer `--json` for
149
160
  machine-readable output and do not depend on interactive pickers or confirms.