@arjunkhera/atlas 0.3.4 → 0.3.6

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "atlas",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, for any repository.",
5
5
  "author": {
6
6
  "name": "Arjun Khera"
package/.mcp.json CHANGED
@@ -7,6 +7,7 @@
7
7
  ],
8
8
  "env": {
9
9
  "ATLAS_ENGRAM": "${user_config.engram_connection}",
10
+ "ATLAS_ENGRAM_CLOUD": "${ATLAS_ENGRAM:-}",
10
11
  "ATLAS_WORK_OWNER_PRESENT": "1",
11
12
  "ATLAS_WORK_JOURNAL": "",
12
13
  "ATLAS_WORK_SESSION": "",
package/door/cli.mjs CHANGED
@@ -54,18 +54,53 @@ Atlas ${ATLAS_VERSION}. Check version ${CHECK_VERSION}; it reads spec ${Object.k
54
54
  A person merges every file the tooling writer writes.
55
55
  `;
56
56
 
57
- // A switch never takes the next word as its value, so it may come before a
58
- // path: `atlas ste --share page.md` reads page.md (a miss on 6 October).
59
- const SWITCHES = new Set(['help', 'share', 'replace-unknown', 'workflow', 'dry-run', 'local', 'skip-global', 'yes']);
57
+ // Each flag of each command, declared once. A test holds this table equal to
58
+ // the flags the handlers read. Three kinds:
59
+ // - switch: never takes a value, so it may come before a path
60
+ // (`atlas ste --share page.md`, a miss on 6 October);
61
+ // - value: takes the next word, and a flag with no value stops the command;
62
+ // - optional: takes the next word unless it is a flag; alone it means the
63
+ // default (`--root` alone is this folder, `upgrade --version` alone is the
64
+ // newest release).
65
+ // Any other flag stops the command with exit 2. A misspelt `--shared` once
66
+ // skipped the privacy filter and printed GREEN (review of PR #107).
67
+ export const FLAGS = Object.freeze({
68
+ tooling: { root: 'optional', branch: 'optional', 'replace-unknown': 'switch', workflow: 'switch', 'dry-run': 'switch' },
69
+ status: { root: 'optional' },
70
+ 'kind-drift': { root: 'optional', 'record-kind': 'value', registry: 'value', repo: 'value' },
71
+ check: { root: 'optional', repo: 'value', 'fail-on': 'value' },
72
+ ste: { share: 'switch', terms: 'value' },
73
+ design: {},
74
+ install: { local: 'switch', from: 'value', 'skip-global': 'switch', yes: 'switch' },
75
+ upgrade: { version: 'optional', yes: 'switch' },
76
+ doctor: {},
77
+ });
60
78
 
61
- function options(argv) {
79
+ // Every command takes --help.
80
+ export function options(argv, declared = {}) {
81
+ const kinds = { help: 'switch', ...declared };
62
82
  const out = { _: [] };
63
83
  for (let index = 0; index < argv.length; index += 1) {
64
- const flag = argv[index];
65
- if (!flag.startsWith('--')) { out._.push(flag); continue; }
66
- const key = flag.slice(2);
84
+ const word = argv[index];
85
+ if (!word.startsWith('--')) { out._.push(word); continue; }
86
+ const equals = word.indexOf('=');
87
+ const key = word.slice(2, equals === -1 ? undefined : equals);
88
+ const kind = Object.hasOwn(kinds, key) ? kinds[key] : null;
89
+ if (!kind) throw new Error(`there is no flag "--${key}" for "atlas ${argv[0]}". Run "atlas help".`);
90
+ if (equals !== -1) {
91
+ if (kind === 'switch') throw new Error(`the flag "--${key}" takes no value.`);
92
+ if (equals === word.length - 1) throw new Error(`the flag "--${key}" needs a value.`);
93
+ out[key] = word.slice(equals + 1);
94
+ continue;
95
+ }
96
+ if (kind === 'switch') { out[key] = true; continue; }
67
97
  const value = argv[index + 1];
68
- if (SWITCHES.has(key) || value === undefined || value.startsWith('--')) { out[key] = true; continue; }
98
+ if (value === undefined || value.startsWith('--')) {
99
+ if (kind === 'value') throw new Error(`the flag "--${key}" needs a value.`);
100
+ out[key] = true;
101
+ continue;
102
+ }
103
+ if (value === '') throw new Error(`the flag "--${key}" needs a value.`);
69
104
  out[key] = value;
70
105
  index += 1;
71
106
  }
@@ -123,10 +158,10 @@ function statusCommand(chosen) {
123
158
 
124
159
  function kindDriftCommand(chosen) {
125
160
  const root = rootOf(chosen);
126
- let recordKind = chosen['record-kind'] === true ? null : chosen['record-kind'] ?? null;
127
- if (!recordKind && chosen.registry && chosen.registry !== true) {
161
+ let recordKind = chosen['record-kind'] ?? null;
162
+ if (!recordKind && chosen.registry) {
128
163
  const kinds = kindsFromRegistry(JSON.parse(readFileSync(resolve(chosen.registry), 'utf8')));
129
- const repo = chosen.repo === true ? null : chosen.repo;
164
+ const repo = chosen.repo ?? null;
130
165
  recordKind = repo ? kinds[repo] ?? null : null;
131
166
  if (!repo) {
132
167
  // One checkout, so only the registry row that names THIS repo can be
@@ -159,7 +194,8 @@ function steCommand(chosen) {
159
194
  const paths = chosen._.slice(1);
160
195
  if (!paths.length) throw new Error('give a file or a folder: atlas ste <path> [--share]');
161
196
  const share = Boolean(chosen.share);
162
- const terms = share ? loadPrivateTerms({ path: typeof chosen.terms === 'string' ? resolve(chosen.terms) : null }) : [];
197
+ if (chosen.terms !== undefined && !share) throw new Error('--terms works only with --share.');
198
+ const terms = share ? loadPrivateTerms({ path: chosen.terms !== undefined ? resolve(chosen.terms) : null }) : [];
163
199
  const results = checkPaths(paths.map((one) => resolve(one)), { share, terms });
164
200
  let count = 0;
165
201
  for (const { file, findings } of results) {
@@ -186,8 +222,8 @@ function designCommand(chosen) {
186
222
 
187
223
  function checkCommand(chosen) {
188
224
  const root = rootOf(chosen);
189
- const report = scan({ root, repoId: chosen.repo === true ? null : chosen.repo ?? null });
190
- const decision = verdict(report, chosen['fail-on'] === true ? 'error' : chosen['fail-on'] ?? 'error');
225
+ const report = scan({ root, repoId: chosen.repo ?? null });
226
+ const decision = verdict(report, chosen['fail-on'] ?? 'error');
191
227
  process.stdout.write(toText(report));
192
228
  line(`${decision.green ? 'GREEN' : 'RED'}: ${decision.why}`);
193
229
  return decision.green ? 0 : 1;
@@ -200,8 +236,10 @@ export async function main(argv = process.argv.slice(2)) {
200
236
  const result = spawnSync(process.execPath, [join(PACKAGE_ROOT, 'work', 'code-read.mjs'), ...argv.slice(1)], { stdio: 'inherit' });
201
237
  return result.status ?? 1;
202
238
  }
203
- const chosen = options(argv);
204
- if (!command || chosen.help || command === 'help') { process.stdout.write(HELP); return 0; }
239
+ if (!command || command === 'help' || command === '--help') { process.stdout.write(HELP); return 0; }
240
+ if (!Object.hasOwn(FLAGS, command)) throw new Error(`there is no verb "${command}". Run "atlas help".`);
241
+ const chosen = options(argv, FLAGS[command]);
242
+ if (chosen.help) { process.stdout.write(HELP); return 0; }
205
243
  switch (command) {
206
244
  case 'install': return install({ packageRoot: PACKAGE_ROOT, options: chosen, line });
207
245
  case 'upgrade': return upgrade({ packageRoot: PACKAGE_ROOT, options: chosen, line });
@@ -67,7 +67,7 @@ const findPlugin = (listed, id) => pluginRows(listed).find((row) => row.id === i
67
67
  export async function install({ packageRoot, options = {}, line = console.log, env = process.env, exec = defaultExec, ask = defaultAsk }) {
68
68
  const { name, version } = packageJson(packageRoot);
69
69
  const local = Boolean(options.local);
70
- const from = options.from && options.from !== true ? String(options.from) : null;
70
+ const from = options.from ? String(options.from) : null;
71
71
  const catalog = catalogFor({ name, version, local });
72
72
  const file = catalogFile(env);
73
73
  const before = existsSync(file) ? readFileSync(file, 'utf8') : null;
@@ -27,10 +27,15 @@ export const PRIVACY_PATTERNS = Object.freeze([
27
27
 
28
28
  const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
29
29
 
30
- // The owner's private terms, or an empty list when the file is not there.
30
+ // The owner's private terms. The default file is optional: no file there
31
+ // means no terms. A file the caller names must exist, because a mistyped
32
+ // name once loaded no terms and passed the page (review of PR #107).
31
33
  export function loadPrivateTerms({ path = null, home = homedir() } = {}) {
32
34
  const file = path ?? join(home, PRIVATE_TERMS_PATH);
33
- if (!existsSync(file)) return [];
35
+ if (!existsSync(file)) {
36
+ if (path) throw new Error(`the private terms file ${path} does not exist.`);
37
+ return [];
38
+ }
34
39
  return readFileSync(file, 'utf8').split('\n').map((line) => line.trim()).filter((line) => line && !line.startsWith('#'));
35
40
  }
36
41
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arjunkhera/atlas",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Atlas: the delivery lifecycle, its crews and the Atlas tools, as a Claude Code plugin for any repository.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/work/mcp.mjs CHANGED
@@ -8,8 +8,11 @@
8
8
  // The Engram address and key arrive together, as one setting, in
9
9
  // ATLAS_ENGRAM. When that setting arrives empty, the server reads the same
10
10
  // value from one owner-only file, ~/.config/atlas/engram-connection (atlas#101:
11
- // Claude Desktop starts the server with the sensitive setting empty). It reads
12
- // them from nowhere else.
11
+ // Claude Desktop starts the server with the sensitive setting empty). In a
12
+ // Claude cloud session only, it reads the cloud environment's own ATLAS_ENGRAM
13
+ // variable first, which .mcp.json passes on as ATLAS_ENGRAM_CLOUD, because the
14
+ // empty plugin setting hides it and a cloud setup script cannot see it. It
15
+ // reads them from nowhere else.
13
16
  import { execFileSync } from 'node:child_process';
14
17
  import { randomUUID } from 'node:crypto';
15
18
  import { resolve, isAbsolute, dirname, join, delimiter } from 'node:path';
@@ -111,6 +114,7 @@ function readConnectionFile(env) {
111
114
  }
112
115
  export function readConnection(env, { allowLoopbackHttp = false, now = Date.now() } = {}) {
113
116
  let raw = unset(env.ATLAS_ENGRAM) ? '' : String(env.ATLAS_ENGRAM).trim();
117
+ if (!raw && inClaudeCloud(env) && !unset(env.ATLAS_ENGRAM_CLOUD)) raw = String(env.ATLAS_ENGRAM_CLOUD).trim();
114
118
  if (!raw) {
115
119
  const file = readConnectionFile(env);
116
120
  if (file.refused) return { state: 'bad', message: `The Engram connection file is refused. ${file.refused}` };