atris 3.34.0 → 3.36.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.
Files changed (163) hide show
  1. package/AGENTS.md +35 -0
  2. package/FOR_AGENTS.md +5 -3
  3. package/README.md +5 -3
  4. package/atris/GETTING_STARTED.md +1 -1
  5. package/atris/atris.md +3 -0
  6. package/atris/policies/day-loop-voice.md +102 -0
  7. package/atris/policies/outbound-artifact-gate.md +2 -0
  8. package/atris/skills/design/SKILL.md +56 -32
  9. package/atris/skills/endgame/SKILL.md +12 -6
  10. package/atris/skills/engines/SKILL.md +35 -8
  11. package/atris/skills/fable-method/SKILL.md +66 -0
  12. package/atris/skills/improve/SKILL.md +65 -45
  13. package/atris/skills/render-cli/SKILL.md +88 -0
  14. package/atris/skills/youtube/SKILL.md +10 -1
  15. package/atris.md +6 -2
  16. package/ax +617 -31
  17. package/bin/atris.js +719 -266
  18. package/commands/activate.js +194 -88
  19. package/commands/aeo.js +52 -0
  20. package/commands/agents.js +166 -0
  21. package/commands/autoland.js +718 -72
  22. package/commands/autopilot-front.js +20 -2
  23. package/commands/autopilot.js +118 -2
  24. package/commands/avail.js +407 -0
  25. package/commands/bench.js +188 -0
  26. package/commands/brain.js +3 -0
  27. package/commands/brief.js +651 -0
  28. package/commands/business-sync.js +192 -6
  29. package/commands/business.js +91 -8
  30. package/commands/clean.js +50 -24
  31. package/commands/close.js +1083 -0
  32. package/commands/cloud.js +245 -0
  33. package/commands/codex-goal.js +26 -2
  34. package/commands/compile.js +292 -1
  35. package/commands/computer.js +150 -3
  36. package/commands/dream.js +365 -0
  37. package/commands/drill.js +371 -0
  38. package/commands/drive.js +187 -0
  39. package/commands/engine.js +1061 -29
  40. package/commands/experiments.js +28 -0
  41. package/commands/feed.js +202 -0
  42. package/commands/feedback.js +34 -12
  43. package/commands/fleet-report.js +206 -0
  44. package/commands/github.js +38 -0
  45. package/commands/gm.js +285 -3
  46. package/commands/goal.js +247 -0
  47. package/commands/improve.js +642 -26
  48. package/commands/init.js +83 -43
  49. package/commands/integrations.js +39 -11
  50. package/commands/interview.js +209 -0
  51. package/commands/land.js +253 -52
  52. package/commands/lesson.js +112 -1
  53. package/commands/lifecycle.js +39 -3
  54. package/commands/linear.js +38 -0
  55. package/commands/log.js +84 -1
  56. package/commands/loops.js +220 -16
  57. package/commands/meet.js +220 -0
  58. package/commands/member.js +899 -66
  59. package/commands/mission.js +3512 -332
  60. package/commands/next.js +137 -0
  61. package/commands/now.js +240 -21
  62. package/commands/one-lap.js +776 -0
  63. package/commands/orb.js +314 -0
  64. package/commands/pack-craft.js +179 -0
  65. package/commands/pack.js +823 -0
  66. package/commands/play.js +3 -2
  67. package/commands/probe.js +30 -3
  68. package/commands/pulse.js +241 -46
  69. package/commands/push.js +260 -82
  70. package/commands/radar.js +259 -14
  71. package/commands/rainmaker.js +49 -0
  72. package/commands/report.js +415 -0
  73. package/commands/scout.js +147 -0
  74. package/commands/search.js +363 -0
  75. package/commands/serve.js +54 -0
  76. package/commands/skill.js +47 -3
  77. package/commands/slop.js +50 -2
  78. package/commands/soul.js +1 -1
  79. package/commands/status.js +50 -5
  80. package/commands/stream.js +861 -0
  81. package/commands/stripe.js +38 -0
  82. package/commands/study.js +693 -0
  83. package/commands/supabase.js +39 -0
  84. package/commands/sync.js +67 -54
  85. package/commands/task.js +2275 -182
  86. package/commands/team.js +73 -0
  87. package/commands/truth.js +29 -3
  88. package/commands/unknowns.js +627 -0
  89. package/commands/update.js +44 -0
  90. package/commands/vercel.js +38 -0
  91. package/commands/verify.js +96 -0
  92. package/commands/watch.js +303 -0
  93. package/commands/wish.js +500 -0
  94. package/commands/workflow.js +11 -5
  95. package/commands/worktree.js +299 -20
  96. package/commands/write.js +399 -0
  97. package/commands/xp.js +29 -11
  98. package/lib/auto-accept-certified.js +391 -43
  99. package/lib/autoland.js +353 -52
  100. package/lib/ax-auto-lane.js +79 -0
  101. package/lib/bench/context.js +147 -0
  102. package/lib/bench/engines.js +141 -0
  103. package/lib/bench/report.js +140 -0
  104. package/lib/bench/runner.js +512 -0
  105. package/lib/brief-ledger.js +350 -0
  106. package/lib/cloud-mission.js +259 -0
  107. package/lib/codex-flight.js +154 -0
  108. package/lib/default-runner.js +45 -0
  109. package/lib/default-verifier.js +70 -0
  110. package/lib/engine-registry.js +232 -0
  111. package/lib/experiments/daily.js +640 -0
  112. package/lib/fleet.js +2431 -38
  113. package/lib/improve-vitals-html.js +171 -0
  114. package/lib/known-commands.js +58 -0
  115. package/lib/loop-doctor.js +416 -0
  116. package/lib/member-switches.js +144 -0
  117. package/lib/memory-view.js +14 -5
  118. package/lib/mission-room.js +1 -0
  119. package/lib/mission-root.js +52 -0
  120. package/lib/mission-runtime-loop.js +7 -0
  121. package/lib/next-moves.js +327 -10
  122. package/lib/official-cli-integration.js +174 -0
  123. package/lib/one-lap-validator.js +60 -0
  124. package/lib/orb-context.js +477 -0
  125. package/lib/orb-scorecard.js +224 -0
  126. package/lib/outbound-send-gate.js +165 -0
  127. package/lib/permission-grants.js +293 -0
  128. package/lib/policy-lessons.js +52 -1
  129. package/lib/pulse.js +277 -3
  130. package/lib/receipt-block.js +168 -0
  131. package/lib/receipt-evidence.js +65 -4
  132. package/lib/review-integrity.js +147 -0
  133. package/lib/router-brain.js +352 -0
  134. package/lib/runner-command.js +33 -0
  135. package/lib/self-drive.js +258 -0
  136. package/lib/short-name.js +103 -0
  137. package/lib/spawn-env.js +18 -0
  138. package/lib/state-detection.js +56 -1
  139. package/lib/sync-status.js +59 -0
  140. package/lib/task-db.js +319 -27
  141. package/lib/task-proof.js +43 -1
  142. package/lib/task-receipt.js +93 -0
  143. package/lib/team-presence.js +260 -0
  144. package/lib/tool-result-encode.js +7 -0
  145. package/lib/trust-tiers.js +90 -0
  146. package/lib/usage.js +107 -0
  147. package/lib/voice-gate.js +163 -0
  148. package/lib/wish-audit.js +1368 -0
  149. package/lib/wish-delegate.js +1840 -0
  150. package/lib/wish-design.js +110 -0
  151. package/lib/wish-stats.js +183 -0
  152. package/lib/wish-store.js +354 -0
  153. package/lib/zip.js +221 -0
  154. package/package.json +3 -1
  155. package/templates/loops/atris/loops/LOOPS.md +55 -0
  156. package/templates/loops/atris/loops/TICK.md +24 -0
  157. package/templates/loops/atris/loops/feedback.md +22 -0
  158. package/templates/loops/atris/loops/quality.md +22 -0
  159. package/templates/loops/atris/wiki/systems/loops.md +41 -0
  160. package/utils/api.js +5 -1
  161. package/utils/auth.js +57 -21
  162. package/utils/update-check.js +27 -6
  163. package/atris/learnings.jsonl +0 -1
@@ -0,0 +1,823 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { getAppBaseUrl, httpRequest } = require('../utils/api');
6
+ const { loadCredentials } = require('../utils/auth');
7
+ const { createZipBuffer, readZipBuffer } = require('../lib/zip');
8
+ const { craftPack } = require('./pack-craft');
9
+
10
+ const REGISTRY_TIMEOUT_MS = 60000;
11
+
12
+ function showHelp() {
13
+ console.log('usage: atris pack craft "<topic>" [--dir <target>] [--force]');
14
+ console.log(' atris pack publish [--dir atris] [--slug <slug>] [--author "<name>"] [--notes "..."] [--minor|--major] [--out <file.zip>] [--push]');
15
+ console.log(' atris pack install <file.zip|url|slug> [--dir <target>] [--force]');
16
+ console.log(' atris pack pull [<slug>] [--dir <path>]');
17
+ console.log(' atris pack status [--dir <path>]');
18
+ console.log(' atris pack update [<dir>]');
19
+ console.log(' atris pack list [--dir <path>]');
20
+ }
21
+
22
+ function slugify(value, fallback = 'atris-pack') {
23
+ return String(value || fallback)
24
+ .toLowerCase()
25
+ .replace(/[^a-z0-9]+/g, '-')
26
+ .replace(/^-+|-+$/g, '') || fallback;
27
+ }
28
+
29
+ function titleFromSlug(slug) {
30
+ return String(slug || 'atris-pack')
31
+ .split('-')
32
+ .filter(Boolean)
33
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
34
+ .join(' ') || 'Atris Pack';
35
+ }
36
+
37
+ function takeValue(args, name) {
38
+ const prefix = `${name}=`;
39
+ for (let i = 0; i < args.length; i += 1) {
40
+ const arg = String(args[i]);
41
+ if (arg.startsWith(prefix)) {
42
+ args.splice(i, 1);
43
+ return arg.slice(prefix.length);
44
+ }
45
+ if (arg === name) {
46
+ const value = args[i + 1];
47
+ if (value === undefined || String(value).startsWith('--')) {
48
+ throw new Error(`${name} requires a value`);
49
+ }
50
+ args.splice(i, 2);
51
+ return value;
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+
57
+ function takeFlag(args, name) {
58
+ const index = args.indexOf(name);
59
+ if (index === -1) return false;
60
+ args.splice(index, 1);
61
+ return true;
62
+ }
63
+
64
+ function parseSemver(value) {
65
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(String(value || ''));
66
+ if (!match) return null;
67
+ return match.slice(1).map((part) => Number(part));
68
+ }
69
+
70
+ function bumpVersion(current, bump) {
71
+ const parsed = parseSemver(current);
72
+ if (!parsed) return '0.1.0';
73
+ const [major, minor, patch] = parsed;
74
+ if (bump === 'major') return `${major + 1}.0.0`;
75
+ if (bump === 'minor') return `${major}.${minor + 1}.0`;
76
+ return `${major}.${minor}.${patch + 1}`;
77
+ }
78
+
79
+ function comparePackVersions(left, right) {
80
+ const leftParsed = parseSemver(left);
81
+ const rightParsed = parseSemver(right);
82
+ if (!leftParsed || !rightParsed) {
83
+ return String(left || '') === String(right || '') ? 0 : null;
84
+ }
85
+ for (let i = 0; i < leftParsed.length; i += 1) {
86
+ if (leftParsed[i] > rightParsed[i]) return 1;
87
+ if (leftParsed[i] < rightParsed[i]) return -1;
88
+ }
89
+ return 0;
90
+ }
91
+
92
+ function readJson(filePath) {
93
+ if (!fs.existsSync(filePath)) return null;
94
+ try {
95
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
96
+ } catch (error) {
97
+ throw new Error(`could not read ${filePath}: ${error.message}`);
98
+ }
99
+ }
100
+
101
+ function writeJson(filePath, value) {
102
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
103
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
104
+ }
105
+
106
+ function buildManifest(existing, options) {
107
+ const existingManifest = existing && typeof existing === 'object' ? existing : {};
108
+ const slug = slugify(options.slug || existingManifest.slug || options.fallbackSlug);
109
+ const version = existingManifest.version
110
+ ? bumpVersion(existingManifest.version, options.bump)
111
+ : '0.1.0';
112
+ const title = existingManifest.title || titleFromSlug(slug);
113
+ const manifest = {
114
+ name: existingManifest.name || slug,
115
+ slug,
116
+ title,
117
+ description: existingManifest.description || `Atris pack for ${title}.`,
118
+ author: (options.author && String(options.author).trim()) || existingManifest.author || '',
119
+ tags: Array.isArray(existingManifest.tags) ? existingManifest.tags : [],
120
+ version,
121
+ versions: Array.isArray(existingManifest.versions) ? [...existingManifest.versions] : [],
122
+ };
123
+ manifest.versions.push({
124
+ version,
125
+ date: new Date().toISOString(),
126
+ // the registry rejects entries with empty notes, so never write one
127
+ notes: options.notes || 'version bump',
128
+ });
129
+ return manifest;
130
+ }
131
+
132
+ function shouldSkipRelative(relativePath, includeLogs) {
133
+ const normalized = relativePath.replace(/\\/g, '/');
134
+ const parts = normalized.split('/').filter(Boolean);
135
+ const basename = parts[parts.length - 1] || '';
136
+ const lowerBase = basename.toLowerCase();
137
+
138
+ if (parts.includes('.git')) return true;
139
+ for (let i = 0; i < parts.length - 1; i += 1) {
140
+ if (parts[i] === '.atris' && parts[i + 1] === 'state') return true;
141
+ }
142
+ if (!includeLogs && parts[0] === 'logs') return true;
143
+ if (lowerBase.startsWith('credentials')) return true;
144
+ if (lowerBase.startsWith('.env')) return true;
145
+ if (lowerBase.endsWith('.pem')) return true;
146
+ if (lowerBase.endsWith('.key')) return true;
147
+ if (lowerBase.startsWith('id_rsa')) return true;
148
+ return false;
149
+ }
150
+
151
+ function collectAtrisEntries(sourceDir, includeLogs) {
152
+ const entries = [];
153
+
154
+ function walk(dir, relativeDir = '') {
155
+ const names = fs.readdirSync(dir).sort();
156
+ for (const name of names) {
157
+ const abs = path.join(dir, name);
158
+ const rel = relativeDir ? path.join(relativeDir, name) : name;
159
+ if (shouldSkipRelative(rel, includeLogs)) continue;
160
+ const stat = fs.statSync(abs);
161
+ if (stat.isDirectory()) {
162
+ walk(abs, rel);
163
+ } else if (stat.isFile()) {
164
+ entries.push({
165
+ name: `atris/${rel.replace(/\\/g, '/')}`,
166
+ data: fs.readFileSync(abs),
167
+ mtime: stat.mtime,
168
+ });
169
+ }
170
+ }
171
+ }
172
+
173
+ walk(sourceDir);
174
+ return entries;
175
+ }
176
+
177
+ // A crafted pack keeps wiki/, README.md, and friends at the pack root, so a
178
+ // root publish must ship the whole folder, not just atris/ (2026-07-09: the
179
+ // first dogfooded pack shipped without its research).
180
+ function collectPackRootEntries(rootDir, includeLogs) {
181
+ const entries = [];
182
+
183
+ function walk(dir, relativeDir = '') {
184
+ const names = fs.readdirSync(dir).sort();
185
+ for (const name of names) {
186
+ const abs = path.join(dir, name);
187
+ const rel = relativeDir ? path.join(relativeDir, name) : name;
188
+ const normalized = rel.replace(/\\/g, '/');
189
+ if (normalized === 'pack.json') continue;
190
+ if (normalized === '.upstream' || normalized.startsWith('.upstream/')) continue;
191
+ if (name === 'node_modules' || name === '.atris' || name === '.DS_Store') continue;
192
+ if (normalized === 'atris/logs' || normalized.startsWith('atris/logs/')) {
193
+ if (!includeLogs) continue;
194
+ }
195
+ if (shouldSkipRelative(normalized, includeLogs)) continue;
196
+ const stat = fs.statSync(abs);
197
+ if (stat.isDirectory()) {
198
+ walk(abs, rel);
199
+ } else if (stat.isFile()) {
200
+ entries.push({ name: normalized, data: fs.readFileSync(abs), mtime: stat.mtime });
201
+ }
202
+ }
203
+ }
204
+
205
+ walk(rootDir);
206
+ return entries;
207
+ }
208
+
209
+ function parseJsonBody(buffer) {
210
+ const text = Buffer.isBuffer(buffer) ? buffer.toString('utf8') : String(buffer || '');
211
+ if (!text.trim()) return { data: null, text };
212
+ try {
213
+ return { data: JSON.parse(text), text };
214
+ } catch {
215
+ return { data: null, text };
216
+ }
217
+ }
218
+
219
+ function responseErrorText(response, fallback) {
220
+ const parsed = parseJsonBody(response.body);
221
+ const data = parsed.data;
222
+ if (data && typeof data === 'object') {
223
+ const message = data.error || data.message || data.detail;
224
+ if (message) return String(message);
225
+ }
226
+ if (parsed.text.trim()) return parsed.text.trim();
227
+ return fallback;
228
+ }
229
+
230
+ function registryUrl(pathname, deps = {}) {
231
+ const appBase = (deps.getAppBaseUrl || getAppBaseUrl)();
232
+ const cleanBase = String(appBase || '').replace(/\/+$/, '');
233
+ return `${cleanBase}${pathname}`;
234
+ }
235
+
236
+ // The web app's CSRF gate only trusts requests whose Origin matches its own
237
+ // url; bearer-token CLI posts are not a CSRF vector, so we present the app
238
+ // origin we are posting to (2026-07-09: prod publish --push failed 403 while
239
+ // every test passed against a bare local server).
240
+ function registryOrigin(deps = {}) {
241
+ const appBase = (deps.getAppBaseUrl || getAppBaseUrl)();
242
+ try {
243
+ return new URL(String(appBase)).origin;
244
+ } catch {
245
+ return '';
246
+ }
247
+ }
248
+
249
+ function optionalAuthHeaders(deps = {}) {
250
+ const readCredentials = deps.loadCredentials || loadCredentials;
251
+ const credentials = readCredentials();
252
+ return credentials && credentials.token ? { Authorization: `Bearer ${credentials.token}` } : {};
253
+ }
254
+
255
+ function requiredAuthHeaders(deps = {}) {
256
+ const headers = optionalAuthHeaders(deps);
257
+ if (!headers.Authorization) {
258
+ throw new Error('not logged in. run atris login first to publish packs.');
259
+ }
260
+ return headers;
261
+ }
262
+
263
+ async function postPackToRegistry(manifest, zipBuffer, deps = {}) {
264
+ const request = deps.httpRequest || httpRequest;
265
+ const url = registryUrl('/api/pack/registry', deps);
266
+ const body = JSON.stringify({ manifest, zipBase64: zipBuffer.toString('base64') });
267
+ const authHeaders = requiredAuthHeaders(deps);
268
+ if (!manifest.author || !String(manifest.author).trim()) {
269
+ throw new Error('registry packs need an author. re-run with --author "<your name>" (or set "author" in pack.json).');
270
+ }
271
+ let response;
272
+ try {
273
+ response = await request(url, {
274
+ method: 'POST',
275
+ timeoutMs: REGISTRY_TIMEOUT_MS,
276
+ headers: {
277
+ ...authHeaders,
278
+ Accept: 'application/json',
279
+ 'Content-Type': 'application/json',
280
+ ...(registryOrigin(deps) ? { Origin: registryOrigin(deps) } : {}),
281
+ },
282
+ body,
283
+ });
284
+ } catch {
285
+ throw new Error('could not reach pack registry. check your connection and try again.');
286
+ }
287
+
288
+ const parsed = parseJsonBody(response.body);
289
+ if (response.status < 200 || response.status >= 300) {
290
+ throw new Error(responseErrorText(response, `registry publish failed with status ${response.status}`));
291
+ }
292
+ if (!parsed.data || typeof parsed.data !== 'object' || parsed.data.ok === false) {
293
+ throw new Error(responseErrorText(response, 'registry publish failed'));
294
+ }
295
+ return parsed.data;
296
+ }
297
+
298
+ async function fetchRegistryZip(slug, deps = {}) {
299
+ const request = deps.httpRequest || httpRequest;
300
+ const url = registryUrl(`/api/pack/registry/${encodeURIComponent(slug)}`, deps);
301
+ let response;
302
+ try {
303
+ response = await request(url, {
304
+ method: 'GET',
305
+ timeoutMs: REGISTRY_TIMEOUT_MS,
306
+ headers: optionalAuthHeaders(deps),
307
+ });
308
+ } catch {
309
+ throw new Error('could not reach pack registry. check your connection and try again.');
310
+ }
311
+ if (response.status < 200 || response.status >= 300) {
312
+ throw new Error(responseErrorText(response, `registry lookup failed for ${slug} with status ${response.status}`));
313
+ }
314
+ if (!response.body || response.body.length === 0) {
315
+ throw new Error(`registry returned an empty zip for ${slug}`);
316
+ }
317
+ return response.body;
318
+ }
319
+
320
+ async function publishPack(rawArgs, cwd = process.cwd(), options = {}) {
321
+ const args = [...rawArgs];
322
+ const dirArg = takeValue(args, '--dir');
323
+ const packRootMode = !dirArg && fs.existsSync(path.join(cwd, 'pack.json'));
324
+ const sourceDir = packRootMode ? path.resolve(cwd) : path.resolve(cwd, dirArg || 'atris');
325
+ const slug = takeValue(args, '--slug');
326
+ const author = takeValue(args, '--author');
327
+ const notes = takeValue(args, '--notes') || '';
328
+ const out = takeValue(args, '--out');
329
+ const includeLogs = takeFlag(args, '--include-logs');
330
+ const major = takeFlag(args, '--major');
331
+ const minor = takeFlag(args, '--minor');
332
+ const push = takeFlag(args, '--push');
333
+ if (major && minor) throw new Error('choose either --major or --minor, not both');
334
+ if (args.length) throw new Error(`unknown pack publish argument: ${args.join(' ')}`);
335
+ if (!fs.existsSync(sourceDir) || !fs.statSync(sourceDir).isDirectory()) {
336
+ throw new Error(`pack source not found: ${path.relative(cwd, sourceDir) || sourceDir}`);
337
+ }
338
+
339
+ const manifestPath = packRootMode
340
+ ? path.join(sourceDir, 'pack.json')
341
+ : path.join(path.dirname(sourceDir), 'pack.json');
342
+ const existing = readJson(manifestPath);
343
+ const manifest = buildManifest(existing, {
344
+ slug,
345
+ author,
346
+ notes,
347
+ bump: major ? 'major' : minor ? 'minor' : 'patch',
348
+ fallbackSlug: packRootMode
349
+ ? path.basename(sourceDir)
350
+ : path.basename(path.dirname(sourceDir)) || path.basename(sourceDir),
351
+ });
352
+ writeJson(manifestPath, manifest);
353
+
354
+ const entries = [
355
+ { name: 'pack.json', data: Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, 'utf8'), mtime: new Date() },
356
+ ...(packRootMode
357
+ ? collectPackRootEntries(sourceDir, includeLogs)
358
+ : collectAtrisEntries(sourceDir, includeLogs)),
359
+ ];
360
+ const zipBuffer = out || push ? createZipBuffer(entries) : null;
361
+ if (out) {
362
+ const outPath = path.resolve(cwd, out);
363
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
364
+ fs.writeFileSync(outPath, zipBuffer);
365
+ console.log(`packed ${manifest.slug} ${manifest.version} -> ${path.relative(cwd, outPath) || outPath}`);
366
+ }
367
+ if (push) {
368
+ const result = await postPackToRegistry(manifest, zipBuffer, options.deps || {});
369
+ const publishedSlug = result.slug || manifest.slug;
370
+ const publishedVersion = result.version || manifest.version;
371
+ console.log(`published ${publishedSlug} ${publishedVersion}`);
372
+ console.log(`install with: atris pack install ${publishedSlug}`);
373
+ }
374
+ if (!out && !push) {
375
+ console.log(`wrote pack.json for ${manifest.slug} ${manifest.version}`);
376
+ console.log('share with: atris pack publish --out <file.zip> or atris pack publish --push');
377
+ if (!manifest.author || !String(manifest.author).trim()) {
378
+ console.log('note: publishing to the registry needs an author. add --author "<your name>".');
379
+ }
380
+ }
381
+ return 0;
382
+ }
383
+
384
+ function shellQuote(value) {
385
+ const text = String(value || '.');
386
+ if (/^[A-Za-z0-9_./:-]+$/.test(text)) return text;
387
+ return `'${text.replace(/'/g, "'\\''")}'`;
388
+ }
389
+
390
+ function fallbackSlugFromZipPath(zipPath) {
391
+ return slugify(path.basename(zipPath, path.extname(zipPath)), 'atris-pack');
392
+ }
393
+
394
+ async function loadZipPayload(source, cwd, deps = {}) {
395
+ const request = deps.httpRequest || httpRequest;
396
+ if (/^https?:\/\//i.test(source)) {
397
+ const response = await request(source, { method: 'GET' });
398
+ if (response.status < 200 || response.status >= 300) {
399
+ throw new Error(`download failed with status ${response.status}`);
400
+ }
401
+ const urlPath = new URL(source).pathname;
402
+ return {
403
+ buffer: response.body,
404
+ fallbackSlug: fallbackSlugFromZipPath(urlPath || 'atris-pack.zip'),
405
+ sourceType: 'url',
406
+ sourceUrl: source,
407
+ };
408
+ }
409
+
410
+ const localPath = path.resolve(cwd, source);
411
+ if (fs.existsSync(localPath) || source.toLowerCase().endsWith('.zip')) {
412
+ if (!fs.existsSync(localPath)) throw new Error(`zip file not found: ${source}`);
413
+ return {
414
+ buffer: fs.readFileSync(localPath),
415
+ fallbackSlug: fallbackSlugFromZipPath(localPath),
416
+ sourceType: 'file',
417
+ };
418
+ }
419
+
420
+ const slug = slugify(source);
421
+ const readCredentials = deps.loadCredentials || loadCredentials;
422
+ const credentials = readCredentials();
423
+ const headers = credentials && credentials.token ? { Authorization: `Bearer ${credentials.token}` } : {};
424
+ const apiBaseUrl = deps.getAppBaseUrl || getAppBaseUrl;
425
+ const url = `${apiBaseUrl()}/api/pack/registry/${encodeURIComponent(slug)}`;
426
+ const response = await request(url, { method: 'GET', headers });
427
+ if (response.status < 200 || response.status >= 300) {
428
+ throw new Error(`registry lookup failed for ${slug} with status ${response.status}`);
429
+ }
430
+ return { buffer: response.body, fallbackSlug: slug, sourceType: 'registry', sourceSlug: slug };
431
+ }
432
+
433
+ function parseManifest(entries, fallbackSlug) {
434
+ const manifestEntry = entries.find((entry) => entry.name === 'pack.json');
435
+ if (!manifestEntry) return { slug: fallbackSlug };
436
+ try {
437
+ const parsed = JSON.parse(manifestEntry.data.toString('utf8'));
438
+ return parsed && typeof parsed === 'object' ? parsed : { slug: fallbackSlug };
439
+ } catch {
440
+ return { slug: fallbackSlug };
441
+ }
442
+ }
443
+
444
+ function manifestVersion(manifest) {
445
+ return manifest.version
446
+ || (Array.isArray(manifest.versions) && manifest.versions[0] && manifest.versions[0].version)
447
+ || 'unknown';
448
+ }
449
+
450
+ function upstreamStatePath(packDir) {
451
+ return path.join(packDir, '.upstream', 'STATE.json');
452
+ }
453
+
454
+ function buildUpstreamState(slug, localVersion, remoteVersion) {
455
+ return {
456
+ slug,
457
+ localVersion,
458
+ remoteVersion,
459
+ pulledAt: new Date().toISOString(),
460
+ };
461
+ }
462
+
463
+ function writeUpstreamState(packDir, state) {
464
+ writeJson(upstreamStatePath(packDir), state);
465
+ }
466
+
467
+ function writeUpstreamZip(entries, packDir, state) {
468
+ const upstreamDir = path.join(packDir, '.upstream');
469
+ fs.rmSync(upstreamDir, { recursive: true, force: true });
470
+ fs.mkdirSync(upstreamDir, { recursive: true });
471
+ writeZipEntries(entries, upstreamDir);
472
+ writeUpstreamState(packDir, state);
473
+ }
474
+
475
+ async function pullPack(rawArgs, cwd = process.cwd(), options = {}) {
476
+ const args = [...rawArgs];
477
+ const packDir = path.resolve(cwd, takeValue(args, '--dir') || '.');
478
+ const slugArg = args.shift() || null;
479
+ if (args.length) throw new Error(`unknown pack pull argument: ${args.join(' ')}`);
480
+
481
+ const existing = readPackManifestFromDir(packDir);
482
+ const slug = slugify(slugArg || existing.slug);
483
+ const zipBuffer = await fetchRegistryZip(slug, options.deps || {});
484
+ const entries = readZipBuffer(zipBuffer);
485
+ const remoteManifest = parseManifest(entries, slug);
486
+ if (slugify(remoteManifest.slug || slug) !== slug) {
487
+ throw new Error(`registry returned different slug: ${remoteManifest.slug}`);
488
+ }
489
+
490
+ const localVersion = manifestVersion(existing);
491
+ const remoteVersion = manifestVersion(remoteManifest);
492
+ const comparison = comparePackVersions(remoteVersion, localVersion);
493
+ if (comparison === null) {
494
+ throw new Error(`could not compare pack versions: local ${localVersion}, remote ${remoteVersion}`);
495
+ }
496
+
497
+ const state = buildUpstreamState(slug, localVersion, remoteVersion);
498
+ if (comparison > 0) {
499
+ writeUpstreamZip(entries, packDir, state);
500
+ console.log(`pulled ${slug} local v${localVersion} -> remote v${remoteVersion}`);
501
+ console.log('upstream lives in .upstream/ for a deliberate merge.');
502
+ return { ok: true, upToDate: false, localVersion, remoteVersion, targetDir: packDir };
503
+ }
504
+
505
+ writeUpstreamState(packDir, state);
506
+ if (comparison === 0) {
507
+ console.log(`already up to date v${localVersion}`);
508
+ } else {
509
+ console.log(`local v${localVersion} is newer than remote v${remoteVersion}`);
510
+ }
511
+ return { ok: true, upToDate: true, localVersion, remoteVersion, targetDir: packDir };
512
+ }
513
+
514
+ function statusPack(rawArgs, cwd = process.cwd()) {
515
+ const args = [...rawArgs];
516
+ const packDir = path.resolve(cwd, takeValue(args, '--dir') || '.');
517
+ if (args.length) throw new Error(`unknown pack status argument: ${args.join(' ')}`);
518
+
519
+ const manifest = readPackManifestFromDir(packDir);
520
+ const localVersion = manifestVersion(manifest);
521
+ const state = readJson(upstreamStatePath(packDir));
522
+ if (!state) {
523
+ console.log(`${manifest.slug} local v${localVersion}, remote never pulled`);
524
+ return { ok: true, manifest, state: null };
525
+ }
526
+
527
+ const remoteVersion = state.remoteVersion || 'unknown';
528
+ console.log(`${state.slug || manifest.slug} local v${localVersion}, last pulled remote v${remoteVersion}`);
529
+ if (state.pulledAt) console.log(`pulled at ${state.pulledAt}`);
530
+ return { ok: true, manifest, state };
531
+ }
532
+
533
+ function originForPayload(payload) {
534
+ if (payload.sourceType === 'registry') {
535
+ return { type: 'registry', slug: payload.sourceSlug };
536
+ }
537
+ if (payload.sourceType === 'url') {
538
+ return { type: 'url', url: payload.sourceUrl };
539
+ }
540
+ if (payload.sourceType === 'file') {
541
+ return { type: 'file' };
542
+ }
543
+ return null;
544
+ }
545
+
546
+ function stampOriginInManifest(manifest, origin) {
547
+ if (!origin || manifest.origin) return manifest;
548
+ return { ...manifest, origin };
549
+ }
550
+
551
+ function writeInstalledPackJson(targetDir, manifest) {
552
+ writeJson(path.join(targetDir, 'pack.json'), manifest);
553
+ }
554
+
555
+ function finalizeInstalledPack(targetDir, payload, preserveOrigin) {
556
+ const manifest = readPackManifestFromDir(targetDir);
557
+ let finalized;
558
+ if (preserveOrigin) {
559
+ finalized = { ...manifest, origin: preserveOrigin };
560
+ } else {
561
+ finalized = stampOriginInManifest(manifest, originForPayload(payload));
562
+ }
563
+ if (finalized !== manifest) {
564
+ writeInstalledPackJson(targetDir, finalized);
565
+ }
566
+ return finalized;
567
+ }
568
+
569
+ function assertForceInstallAllowed(targetDir, slug) {
570
+ if (!fs.existsSync(targetDir)) return null;
571
+ const manifestPath = path.join(targetDir, 'pack.json');
572
+ if (!fs.existsSync(manifestPath)) return null;
573
+ const existing = readPackManifestFromDir(targetDir);
574
+ if (slugify(existing.slug) !== slugify(slug)) {
575
+ throw new Error(
576
+ `refusing update: existing slug ${existing.slug} does not match ${slug}`,
577
+ );
578
+ }
579
+ return existing;
580
+ }
581
+
582
+ function writeZipEntries(entries, targetDir) {
583
+ const writes = [];
584
+ for (const entry of entries) {
585
+ if (!entry.name || entry.name.endsWith('/')) continue;
586
+ writes.push({ destination: resolveEntryTarget(targetDir, entry.name), data: entry.data });
587
+ }
588
+ for (const write of writes) {
589
+ fs.mkdirSync(path.dirname(write.destination), { recursive: true });
590
+ fs.writeFileSync(write.destination, write.data);
591
+ }
592
+ }
593
+
594
+ function resolveEntryTarget(targetDir, entryName) {
595
+ const normalized = String(entryName || '').replace(/\\/g, '/');
596
+ const parts = normalized.split('/').filter(Boolean);
597
+ if (!normalized || normalized.startsWith('/') || path.isAbsolute(normalized) || parts.includes('..')) {
598
+ throw new Error(`refusing zip entry outside target: ${entryName}`);
599
+ }
600
+ const targetRoot = path.resolve(targetDir);
601
+ const destination = path.resolve(targetRoot, ...parts);
602
+ const rootWithSep = `${targetRoot}${path.sep}`;
603
+ if (destination !== targetRoot && !destination.startsWith(rootWithSep)) {
604
+ throw new Error(`refusing zip entry outside target: ${entryName}`);
605
+ }
606
+ return destination;
607
+ }
608
+
609
+ async function installPack(rawArgs, cwd = process.cwd(), options = {}) {
610
+ const args = [...rawArgs];
611
+ const source = args.shift();
612
+ if (!source || source === 'help' || source === '--help' || source === '-h') {
613
+ showHelp();
614
+ return source ? 0 : 2;
615
+ }
616
+ const targetArg = takeValue(args, '--dir');
617
+ const force = takeFlag(args, '--force');
618
+ if (args.length) throw new Error(`unknown pack install argument: ${args.join(' ')}`);
619
+
620
+ const payload = await loadZipPayload(source, cwd, options.deps || {});
621
+ const entries = readZipBuffer(payload.buffer);
622
+ const zipManifest = parseManifest(entries, payload.fallbackSlug);
623
+ const slug = slugify(zipManifest.slug || payload.fallbackSlug);
624
+ const targetDir = path.resolve(cwd, targetArg || slug);
625
+ if (fs.existsSync(path.join(targetDir, 'atris')) && !force) {
626
+ throw new Error(`target already contains atris/: ${path.relative(cwd, targetDir) || targetDir}. rerun with --force to overwrite.`);
627
+ }
628
+
629
+ const existing = force ? assertForceInstallAllowed(targetDir, slug) : null;
630
+ const preserveOrigin = existing && existing.origin ? existing.origin : null;
631
+
632
+ writeZipEntries(entries, targetDir);
633
+ const manifest = finalizeInstalledPack(targetDir, payload, preserveOrigin);
634
+
635
+ if (force && existing) {
636
+ const oldVersion = manifestVersion(existing);
637
+ const newVersion = manifestVersion(manifest);
638
+ console.log(`updated v${oldVersion} -> v${newVersion}`);
639
+ }
640
+
641
+ const displayTarget = path.relative(cwd, targetDir) || '.';
642
+ console.log(`installed ${slug} -> ${displayTarget}`);
643
+ console.log(`cd ${shellQuote(displayTarget)} && claude`);
644
+ return 0;
645
+ }
646
+
647
+ async function updatePack(rawArgs, cwd = process.cwd(), options = {}) {
648
+ const args = [...rawArgs];
649
+ const packDir = args.shift() || '.';
650
+ if (args.length) throw new Error(`unknown pack update argument: ${args.join(' ')}`);
651
+
652
+ const resolved = path.resolve(cwd, packDir);
653
+ const existing = readPackManifestFromDir(resolved);
654
+ const origin = existing.origin;
655
+
656
+ if (!origin || typeof origin !== 'object' || !origin.type) {
657
+ throw new Error('pack.json is missing origin; reinstall or use pack install --force');
658
+ }
659
+ if (origin.type === 'file') {
660
+ throw new Error(
661
+ 'this pack was installed from a local zip; update it with: atris pack install <zip> --dir <path> --force',
662
+ );
663
+ }
664
+ if (origin.type !== 'registry' && origin.type !== 'url') {
665
+ throw new Error(`unsupported origin type: ${origin.type}`);
666
+ }
667
+
668
+ const deps = options.deps || {};
669
+ const request = deps.httpRequest || httpRequest;
670
+ const url = origin.type === 'registry'
671
+ ? `${(deps.getAppBaseUrl || getAppBaseUrl)()}/api/pack/registry/${encodeURIComponent(origin.slug)}`
672
+ : origin.url;
673
+ const readCredentials = deps.loadCredentials || loadCredentials;
674
+ const credentials = readCredentials();
675
+ const headers = credentials && credentials.token ? { Authorization: `Bearer ${credentials.token}` } : {};
676
+ const response = await request(url, { method: 'GET', headers });
677
+ if (response.status < 200 || response.status >= 300) {
678
+ throw new Error(`download failed with status ${response.status}`);
679
+ }
680
+
681
+ const entries = readZipBuffer(response.body);
682
+ const zipManifest = parseManifest(entries, existing.slug);
683
+ if (slugify(zipManifest.slug) !== slugify(existing.slug)) {
684
+ throw new Error(`download returned different slug: ${zipManifest.slug}`);
685
+ }
686
+
687
+ const oldVersion = manifestVersion(existing);
688
+ const newVersion = manifestVersion(zipManifest);
689
+ if (oldVersion === newVersion) {
690
+ console.log(`already up to date v${oldVersion}`);
691
+ return { ok: true, upToDate: true, manifest: existing, targetDir: resolved };
692
+ }
693
+
694
+ writeZipEntries(entries, resolved);
695
+ const installed = readPackManifestFromDir(resolved);
696
+ const manifest = { ...installed, origin: existing.origin };
697
+ writeInstalledPackJson(resolved, manifest);
698
+ console.log(`updated v${oldVersion} -> v${newVersion}`);
699
+ return { ok: true, upToDate: false, manifest, targetDir: resolved };
700
+ }
701
+
702
+ function readPackManifestFromDir(packDir) {
703
+ const manifest = readJson(path.join(packDir, 'pack.json'));
704
+ if (!manifest || typeof manifest !== 'object' || !manifest.slug) {
705
+ throw new Error(`pack.json is missing slug: ${packDir}`);
706
+ }
707
+ return manifest;
708
+ }
709
+
710
+ function collectPackDirs(root, found, seen, depth = 0) {
711
+ const resolved = path.resolve(root);
712
+ if (seen.has(resolved) || depth > 4) return;
713
+ seen.add(resolved);
714
+
715
+ let entries;
716
+ try {
717
+ entries = fs.readdirSync(resolved, { withFileTypes: true });
718
+ } catch {
719
+ return;
720
+ }
721
+
722
+ const hasPackJson = entries.some((entry) => entry.isFile() && entry.name === 'pack.json');
723
+ if (hasPackJson) {
724
+ try {
725
+ found.push({ dir: resolved, manifest: readPackManifestFromDir(resolved) });
726
+ } catch {
727
+ // Ignore malformed pack folders while listing installed packs.
728
+ }
729
+ return;
730
+ }
731
+
732
+ for (const entry of entries) {
733
+ if (!entry.isDirectory()) continue;
734
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === '.atris') continue;
735
+ collectPackDirs(path.join(resolved, entry.name), found, seen, depth + 1);
736
+ }
737
+ }
738
+
739
+ function listInstalledPacks(cwd = process.cwd()) {
740
+ const root = path.resolve(cwd);
741
+ const found = [];
742
+ const seen = new Set();
743
+ collectPackDirs(root, found, seen);
744
+ collectPackDirs(path.join(root, 'packs'), found, seen);
745
+
746
+ found.sort((a, b) => {
747
+ const left = String(a.manifest.title || a.manifest.name || a.manifest.slug);
748
+ const right = String(b.manifest.title || b.manifest.name || b.manifest.slug);
749
+ return left.localeCompare(right);
750
+ });
751
+
752
+ if (!found.length) {
753
+ console.log('no packs found under this directory');
754
+ return { packs: [] };
755
+ }
756
+
757
+ for (const item of found) {
758
+ const title = item.manifest.title || item.manifest.name || item.manifest.slug;
759
+ const version = item.manifest.version || 'unknown';
760
+ const displayPath = path.relative(root, item.dir) || '.';
761
+ console.log(`${item.manifest.slug} ${title} v${version} ${displayPath}`);
762
+ }
763
+ return { packs: found };
764
+ }
765
+
766
+ function listPackCommand(rawArgs, cwd = process.cwd()) {
767
+ const args = [...rawArgs];
768
+ const dir = takeValue(args, '--dir');
769
+ if (args.length) throw new Error(`unknown pack list argument: ${args.join(' ')}`);
770
+ return listInstalledPacks(dir ? path.resolve(cwd, dir) : cwd);
771
+ }
772
+
773
+ async function run(argv = []) {
774
+ const [subcommand, ...args] = argv;
775
+ try {
776
+ if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
777
+ showHelp();
778
+ return subcommand ? 0 : 2;
779
+ }
780
+ if (subcommand === 'craft') {
781
+ const result = craftPack(args);
782
+ if (result && result.needsHelp) {
783
+ showHelp();
784
+ return 0;
785
+ }
786
+ return result;
787
+ }
788
+ if (subcommand === 'publish') return await publishPack(args);
789
+ if (subcommand === 'install') return await installPack(args);
790
+ if (subcommand === 'pull') return await pullPack(args);
791
+ if (subcommand === 'status') return statusPack(args);
792
+ if (subcommand === 'update') return await updatePack(args);
793
+ if (subcommand === 'list') return listPackCommand(args);
794
+ console.error(`unknown pack command: ${subcommand}`);
795
+ showHelp();
796
+ return 2;
797
+ } catch (error) {
798
+ console.error(error.message || String(error));
799
+ return 1;
800
+ }
801
+ }
802
+
803
+ module.exports = {
804
+ run,
805
+ craftPack,
806
+ publishPack,
807
+ installPack,
808
+ pullPack,
809
+ statusPack,
810
+ updatePack,
811
+ listInstalledPacks,
812
+ loadZipPayload,
813
+ buildManifest,
814
+ comparePackVersions,
815
+ readPackManifestFromDir,
816
+ shouldSkipRelative,
817
+ resolveEntryTarget,
818
+ manifestVersion,
819
+ originForPayload,
820
+ stampOriginInManifest,
821
+ assertForceInstallAllowed,
822
+ writeZipEntries,
823
+ };