@arjunkhera/atlas 0.3.45 → 0.3.48

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.45",
3
+ "version": "0.3.48",
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/door/cli.mjs CHANGED
@@ -44,7 +44,7 @@ const ATLAS_VERSION = packagePackageVersion(PACKAGE_ROOT);
44
44
 
45
45
  const HELP = `atlas — the door into a repo's Atlas files
46
46
 
47
- atlas tooling write --root <repo> write both Atlas files into a repo
47
+ atlas tooling write --root <repo> write the Atlas files into a repo
48
48
  atlas tooling pr --root <repo> the same, as one pull request
49
49
  --replace-unknown also replace a copy that is in no release
50
50
  --workflow rewrite only the workflow, for a repo whose check is current
@@ -140,8 +140,10 @@ const HELP = `atlas — the door into a repo's Atlas files
140
140
  compare the judge's verdict with the sealed tasks and the runs; exit 0 only when all pass
141
141
 
142
142
  atlas install install Atlas for every folder on this Mac
143
+ --cloud in a cloud setup script: skip the checks that need a started session
143
144
  atlas upgrade [--version <v>] check, show and install a newer release
144
145
  atlas doctor is Atlas installed and working here
146
+ --cloud before the first cloud session: skip the checks that need a started session
145
147
 
146
148
  Atlas ${ATLAS_VERSION}. Check version ${CHECK_VERSION}; it reads spec ${Object.keys(SPECS).join(', ')}.
147
149
  A person merges every file the tooling writer writes.
@@ -169,9 +171,9 @@ export const FLAGS = Object.freeze({
169
171
  upkeep: { root: 'optional', event: 'value', json: 'switch' },
170
172
  gate: { repo: 'value', root: 'optional', 'dry-run': 'switch', off: 'switch', on: 'switch' },
171
173
  judge: { tasks: 'value', verdict: 'value', seal: 'value', runs: 'value', out: 'value', repo: 'value', budget: 'value', model: 'value' },
172
- install: { local: 'switch', from: 'value', 'skip-global': 'switch', yes: 'switch' },
174
+ install: { local: 'switch', from: 'value', 'skip-global': 'switch', yes: 'switch', cloud: 'switch' },
173
175
  upgrade: { version: 'optional', yes: 'switch' },
174
- doctor: {},
176
+ doctor: { cloud: 'switch' },
175
177
  });
176
178
 
177
179
  // Every command takes --help.
@@ -230,7 +232,7 @@ function toolingCommand(chosen) {
230
232
  for (const row of result.rows) line(` ${row.state.padEnd(10)} ${row.path} sha256 ${row.sha256.slice(0, 12)}…`);
231
233
  line('');
232
234
  line(result.changed
233
- ? 'Both files come from the package. No hand step, and no edit by hand afterwards.'
235
+ ? 'The files come from the package. No hand step, and no edit by hand afterwards.'
234
236
  : 'Nothing changed. Both files already match the package.');
235
237
  return 0;
236
238
  } catch (error) {
@@ -140,9 +140,12 @@ export async function install({ packageRoot, options = {}, line = console.log, e
140
140
  const installArgs = ['plugin', 'install', PLUGIN_ID, '--scope', 'user'];
141
141
  exec('claude', installArgs);
142
142
  line('');
143
- for (const text of connectionSteps(env)) line(text);
143
+ // A cloud setup script runs before any session: the key comes from the
144
+ // environment variable, and no tools server has started yet (item 92cfbba3).
145
+ if (options.cloud) line('Installed. In a cloud session the key comes from the environment, and the lead checks the version at its first reply.');
146
+ else for (const text of connectionSteps(env)) line(text);
144
147
  line('');
145
- return doctor({ packageRoot, options, line, env, exec });
148
+ return doctor({ packageRoot, options: { cloud: Boolean(options.cloud) }, line, env, exec });
146
149
  }
147
150
 
148
151
  // ---------------------------------------------------------------- upgrade
@@ -151,13 +154,15 @@ export async function install({ packageRoot, options = {}, line = console.log, e
151
154
  export const blobSha = (bytes) => createHash('sha1').update(`blob ${bytes.length}\0`).update(bytes).digest('hex');
152
155
  export const sha512Integrity = (bytes) => `sha512-${createHash('sha512').update(bytes).digest('base64')}`;
153
156
 
154
- function filesUnder(root) {
157
+ function filesUnder(root, skip = () => false) {
155
158
  const out = new Map();
156
159
  const walk = (dir) => {
157
160
  for (const entry of readdirSync(dir)) {
158
161
  const full = join(dir, entry);
162
+ const path = relative(root, full).split('\\').join('/');
163
+ if (skip(path)) continue;
159
164
  if (statSync(full).isDirectory()) walk(full);
160
- else out.set(relative(root, full).split('\\').join('/'), readFileSync(full));
165
+ else out.set(path, readFileSync(full));
161
166
  }
162
167
  };
163
168
  walk(root);
@@ -201,6 +206,36 @@ export function matchTree({ tree, files }) {
201
206
  return problems;
202
207
  }
203
208
 
209
+ // Claude Code writes `.in_use/` into a plugin copy to mark it in use. It is
210
+ // not part of the package, so the proof of a loaded copy skips it.
211
+ const COPY_MARKER = /^\.in_use(\/|$)/;
212
+ const firstLines = (error, count = 3) => String(error?.stderr || error?.message || error).trim().split('\n').slice(0, count).join(' ');
213
+
214
+ // Is the copy at packageRoot the copy that a master release run built? The
215
+ // same proof as the upgrade, run on files already loaded. It reads only.
216
+ // The proof runs from the copy it checks, so it catches stale, broken or
217
+ // drifted files, not a package made to lie (security review of atlas#187).
218
+ // "unproven" is the upgrade's refusal: no master run built this version.
219
+ // "not checked" is never a pass: the caller says the reason.
220
+ export function proveCopy({ packageRoot, exec = defaultExec }) {
221
+ try {
222
+ const { version } = packageJson(packageRoot);
223
+ const repo = repoOf(packageRoot);
224
+ const gh = (args) => JSON.parse(exec('gh', args));
225
+ let run;
226
+ try { run = findReleaseRun({ gh, repo, version }); } catch (error) { return { state: 'not checked', reason: `GitHub did not answer: ${firstLines(error)}` }; }
227
+ if (!run) return { state: 'unproven', reason: `no successful release run on a commit of master built ${version}. It may have been published from a branch.` };
228
+ let tree;
229
+ try { tree = gh(['api', '--method', 'GET', `repos/${repo}/git/trees/${run.head_sha}?recursive=1`]).tree; } catch (error) { return { state: 'not checked', reason: `GitHub did not give the file tree: ${firstLines(error)}` }; }
230
+ const files = filesUnder(packageRoot, (path) => COPY_MARKER.test(path));
231
+ const problems = matchTree({ tree, files });
232
+ const found = { run: run.id, commit: run.head_sha };
233
+ return problems.length ? { state: 'differs', ...found, problems: problems.slice(0, 20) } : { state: 'match', ...found, files: files.size };
234
+ } catch (error) {
235
+ return { state: 'not checked', reason: `the proof stopped: ${firstLines(error)}` };
236
+ }
237
+ }
238
+
204
239
  export async function upgrade({ packageRoot, options = {}, line = console.log, env = process.env, exec = defaultExec, ask = defaultAsk }) {
205
240
  const { name, version: installed } = packageJson(packageRoot);
206
241
  const repo = repoOf(packageRoot);
@@ -276,11 +311,18 @@ export function doctor({ packageRoot, options = {}, line = console.log, env = pr
276
311
  }
277
312
 
278
313
  // Off the cloud, the key lives only in the connection file. The cloud passes it as a variable.
279
- if (env.CLAUDE_CODE_REMOTE !== 'true') {
314
+ if (env.CLAUDE_CODE_REMOTE !== 'true' && !options.cloud) {
280
315
  const connection = readConnection({ HOME: env.HOME });
281
316
  check('connection', connection.state === 'ok', connection.state === 'ok' ? `the Engram connection file is in place; the key expires ${connection.expires}` : connection.message);
282
317
  }
283
318
 
319
+ // A cloud setup runs before the first session, so the three checks of a
320
+ // started tools server cannot pass yet. The lead checks at its first reply.
321
+ if (options.cloud) {
322
+ for (const row of rows) line(`${row.pass ? 'PASS' : 'FAIL'} ${row.name.padEnd(12)} ${row.detail}`);
323
+ line('SKIP tools server, engram and gh: no session has started yet');
324
+ return rows.every((row) => row.pass) ? 0 : 1;
325
+ }
284
326
  let health = null;
285
327
  try { health = JSON.parse(readFileSync(join(env.HOME, '.atlas', 'health.json'), 'utf8')); } catch { /* never started */ }
286
328
  check('tools server', Boolean(health), health ? `last started ${health.started_at}, Atlas ${health.package_version}` : 'the Atlas tools server has not started yet. Open a Claude Code session, then run "atlas doctor" again.');