atris 3.37.1 → 3.38.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 (139) hide show
  1. package/README.md +2 -0
  2. package/atris/PERSONA.md +3 -7
  3. package/atris.md +10 -2
  4. package/ax +158 -33
  5. package/bin/atris.js +49 -2
  6. package/commands/activate.js +16 -3
  7. package/commands/agent-spawn.js +0 -7
  8. package/commands/autoland.js +92 -14
  9. package/commands/autopilot-front.js +0 -4
  10. package/commands/autopilot.js +0 -12
  11. package/commands/brain.js +0 -7
  12. package/commands/brief.js +0 -2
  13. package/commands/business-sync.js +0 -1
  14. package/commands/business.js +107 -12
  15. package/commands/clean.js +31 -2
  16. package/commands/close.js +46 -11
  17. package/commands/cloud.js +0 -1
  18. package/commands/codex-goal.js +0 -2
  19. package/commands/computer.js +6 -5
  20. package/commands/console.js +46 -9
  21. package/commands/deck.js +29 -8
  22. package/commands/dream.js +0 -7
  23. package/commands/drill.js +0 -1
  24. package/commands/engine.js +10 -18
  25. package/commands/experiments.js +0 -4
  26. package/commands/fleet-report.js +12 -4
  27. package/commands/game.js +261 -0
  28. package/commands/gm.js +10 -2
  29. package/commands/goal.js +0 -6
  30. package/commands/harvest.js +0 -1
  31. package/commands/improve.js +29 -8
  32. package/commands/init.js +18 -34
  33. package/commands/integrations.js +0 -7
  34. package/commands/land.js +71 -8
  35. package/commands/launchpad.js +0 -2
  36. package/commands/lesson.js +17 -7
  37. package/commands/loop-front.js +0 -2
  38. package/commands/loops.js +6 -5
  39. package/commands/meet.js +0 -2
  40. package/commands/member.js +68 -3
  41. package/commands/mission.js +359 -102
  42. package/commands/next.js +0 -1
  43. package/commands/now.js +0 -12
  44. package/commands/one-lap.js +8 -8
  45. package/commands/orb.js +0 -4
  46. package/commands/pack-craft.js +0 -1
  47. package/commands/pack.js +670 -77
  48. package/commands/play.js +0 -2
  49. package/commands/probe.js +10 -4
  50. package/commands/pulse.js +73 -48
  51. package/commands/push.js +0 -4
  52. package/commands/radar.js +14 -11
  53. package/commands/recover.js +340 -0
  54. package/commands/report.js +0 -1
  55. package/commands/router.js +319 -0
  56. package/commands/run-front.js +0 -2
  57. package/commands/search.js +0 -3
  58. package/commands/skill.js +7 -2
  59. package/commands/slop.js +225 -7
  60. package/commands/status.js +6 -0
  61. package/commands/stream.js +0 -2
  62. package/commands/study.js +0 -3
  63. package/commands/sync-checkout.js +51 -0
  64. package/commands/sync.js +11 -40
  65. package/commands/task.js +120 -38
  66. package/commands/teach.js +86 -0
  67. package/commands/unknowns.js +0 -10
  68. package/commands/update.js +0 -1
  69. package/commands/visualize.js +0 -5
  70. package/commands/watch.js +0 -6
  71. package/commands/wiki.js +41 -11
  72. package/commands/wish.js +0 -1
  73. package/commands/worktree.js +87 -14
  74. package/commands/xp.js +1 -6
  75. package/commands/youtube.js +0 -2
  76. package/lib/accept-verify-gate.js +122 -0
  77. package/lib/activity-stream.js +0 -2
  78. package/lib/auto-accept-certified.js +145 -2
  79. package/lib/autoland.js +0 -3
  80. package/lib/ax-auto-lane.js +68 -7
  81. package/lib/bench/context.js +0 -2
  82. package/lib/bench/report.js +0 -1
  83. package/lib/bench/runner.js +0 -11
  84. package/lib/brief-ledger.js +0 -6
  85. package/lib/business-simulate.js +382 -0
  86. package/lib/chat-log-scan.js +0 -5
  87. package/lib/checkout-sync.js +52 -0
  88. package/lib/claude-boot-block.js +48 -0
  89. package/lib/cloud-mission.js +0 -6
  90. package/lib/codex-flight.js +0 -1
  91. package/lib/conductor-artifacts.js +25 -0
  92. package/lib/context-gatherer.js +0 -1
  93. package/lib/deck-compose.js +0 -1
  94. package/lib/deck-history.js +0 -1
  95. package/lib/deck-layout.js +0 -5
  96. package/lib/deck-review.js +0 -7
  97. package/lib/deck-schema.js +0 -1
  98. package/lib/default-verifier.js +0 -2
  99. package/lib/engine-registry.js +16 -8
  100. package/lib/experiments/daily.js +0 -6
  101. package/lib/fleet.js +210 -27
  102. package/lib/functional-owner.js +0 -3
  103. package/lib/inspect-fields.js +0 -1
  104. package/lib/known-commands.js +2 -2
  105. package/lib/learnings.js +0 -2
  106. package/lib/loop-doctor.js +11 -17
  107. package/lib/manifest.js +0 -2
  108. package/lib/member-alive.js +0 -1
  109. package/lib/member-switches.js +0 -5
  110. package/lib/mission-artifact.js +0 -1
  111. package/lib/mission-protected-lane.js +293 -0
  112. package/lib/mission-room.js +0 -2
  113. package/lib/mission-runtime-loop.js +0 -1
  114. package/lib/next-moves.js +0 -18
  115. package/lib/official-cli-integration.js +0 -4
  116. package/lib/orb-context.js +6 -3
  117. package/lib/orb-scorecard.js +0 -1
  118. package/lib/outbound-send-gate.js +0 -6
  119. package/lib/permission-grants.js +0 -6
  120. package/lib/policy-lessons.js +0 -2
  121. package/lib/pulse.js +68 -15
  122. package/lib/review-integrity.js +0 -2
  123. package/lib/router-brain.js +0 -2
  124. package/lib/runner-command.js +0 -4
  125. package/lib/runtime-bootstrap.js +0 -1
  126. package/lib/scorecard.js +0 -1
  127. package/lib/self-drive.js +1 -1
  128. package/lib/state-detection.js +0 -6
  129. package/lib/task-db.js +0 -4
  130. package/lib/team-presence.js +0 -1
  131. package/lib/voice-gate.js +63 -2
  132. package/lib/wiki.js +152 -9
  133. package/lib/wish-audit.js +20 -6
  134. package/lib/wish-delegate.js +26 -20
  135. package/lib/wish-stats.js +0 -5
  136. package/lib/wish-store.js +0 -1
  137. package/lib/workspace-safety.js +0 -2
  138. package/lib/zip.js +0 -1
  139. package/package.json +1 -1
package/commands/pack.js CHANGED
@@ -9,10 +9,135 @@ const { craftPack } = require('./pack-craft');
9
9
 
10
10
  const REGISTRY_TIMEOUT_MS = 60000;
11
11
 
12
+ // The web registry caps every upload (atrisos-web app/api/pack/registry/route.ts).
13
+ // Keep these in one place so a preflight failure can name the limit it broke.
14
+ const REGISTRY_LIMITS = {
15
+ maxZipBytes: 5 * 1024 * 1024,
16
+ maxUnpackedBytes: 20 * 1024 * 1024,
17
+ maxEntries: 500,
18
+ };
19
+
20
+ // The web viewer rejects anything else (atrisos-web app/lib/pack/manifests.ts).
21
+ const SLUG_RULE = /^[a-z0-9-]{3,40}$/;
22
+
23
+ // ── The packet allowlist ────────────────────────────────────────────────────
24
+ // A packet is the shareable knowledge spine of a workspace, not the folder it
25
+ // lives in. Only what a stranger could read and learn from ships: the map and
26
+ // readme layer, member and team definitions, the wiki, skills, policies, refs,
27
+ // features. Runtime exhaust (runs/, logs/, journal/, status/, .atris/),
28
+ // dependencies, binaries and lockfiles never ship.
29
+ //
30
+ // This table is the whole rule. Audit it here, not in scattered ifs.
31
+ // (2026-07-27: the denylist that preceded it shipped a 94 MB zip containing
32
+ // node_modules, customer folders and a live JWT.)
33
+ const PACKET = {
34
+ // Folders whose contents ship. Checked at the first level under the pack
35
+ // root and under atris/. Anything not listed here is skipped.
36
+ directories: [
37
+ 'features', // what got built, and why
38
+ 'wiki', // ingested knowledge
39
+ 'skills', // reusable procedures
40
+ 'team', // member and role definitions
41
+ 'policies', // house rules
42
+ 'refs', // stable reference tables
43
+ 'reference',
44
+ 'checklists',
45
+ 'templates',
46
+ 'guides',
47
+ 'docs',
48
+ 'briefs',
49
+ 'playbooks',
50
+ ],
51
+ // Root-level files ship only as documents: the map, the readme, the persona.
52
+ rootExtensions: ['.md', '.markdown', '.txt'],
53
+ // ...plus the manifest itself.
54
+ rootFiles: ['pack.json'],
55
+ // Inside an allowed folder, only these extensions ship. Text only.
56
+ extensions: ['.md', '.markdown', '.txt', '.yml', '.yaml', '.json', '.toml', '.csv'],
57
+ // Path segments that never ship, at any depth. Runtime exhaust and deps.
58
+ deniedSegments: [
59
+ 'runs', 'logs', 'journal', 'status', 'missions', 'queue', 'tmp', 'cache',
60
+ '.atris', '.git', '.upstream', '.venv', 'venv', 'node_modules',
61
+ '__pycache__', 'dist', 'build', 'coverage', '_archive',
62
+ // Proof folders are receipts of runs that already happened, not knowledge a
63
+ // stranger can use. A packet ships the feature definition, not its history.
64
+ // (2026-07-27: features/*/proof was 1,253 files and 76% of the unpacked
65
+ // weight of the atrisos-backend workspace.)
66
+ 'proof',
67
+ // Same class as proof: a receipt records that a run happened. Kept as its
68
+ // own entry so the principle stays legible rather than reading as one
69
+ // grab-bag of excluded words.
70
+ 'receipts',
71
+ ],
72
+ // Segments that get their own skip reason, because "runtime exhaust" does not
73
+ // explain why a folder full of readable markdown was left out.
74
+ segmentReasons: {
75
+ proof: 'proof artifacts are excluded (receipts of past runs, not knowledge)',
76
+ receipts: 'receipts are excluded (records of past runs, not knowledge)',
77
+ },
78
+
79
+ // ── definitions, not state ────────────────────────────────────────────────
80
+ // A packet carries what a thing IS, never what it is currently DOING. The
81
+ // definition files travel — MEMBER.md, MISSION.md, SKILL.md, SOUL.md,
82
+ // README.md, wiki pages, policies. The running state a live workspace keeps
83
+ // beside them stays home: a stranger cannot use someone else's standup notes,
84
+ // and project-management exhaust is most of the file count.
85
+ //
86
+ // The rule is matched on FILENAME, not on folder path, so it generalizes to
87
+ // any Atris workspace instead of naming the folders of one.
88
+ // (2026-07-27, atrisos-backend/atris: team/ was 939 eligible files of which
89
+ // only ~129 were definitions; features/ was 1,183 of which only 43 were
90
+ // README.md. The rest were now.md/goals.md, dated journals, and the
91
+ // idea/build/validate plan-do-review triplet.)
92
+ runningStateNames: [
93
+ 'now.md', // what this member/feature is doing right now
94
+ 'goals.md', // this workspace's current targets
95
+ 'idea.md', // the plan-do-review triplet: an internal tracker, not a
96
+ 'build.md', // description of the thing. features/*/README.md is the
97
+ 'validate.md', // definition and it still ships.
98
+ ],
99
+ // Dated journal entries, at any depth: 2026-07-06.md, 2026-07-06-retro.md.
100
+ runningStatePatterns: [/^\d{4}-\d{2}-\d{2}(?:[-_][^.]*)?\.[a-z0-9]+$/i],
101
+ runningStateReason: 'running state is excluded (a packet carries definitions, not state)',
102
+ // Exact filenames that never ship.
103
+ deniedNames: [
104
+ 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'poetry.lock',
105
+ 'cargo.lock', 'uv.lock', '.ds_store',
106
+ ],
107
+ // Secret-shaped filenames that never ship, at any depth.
108
+ deniedPatterns: [
109
+ /^credentials/i, /^\.env/i, /\.pem$/i, /\.key$/i, /^id_rsa/i, /^id_ed25519/i,
110
+ ],
111
+ };
112
+
113
+ // Credential shapes. A hit blocks the publish; the match is redacted, never
114
+ // echoed, because the point of the scan is to stop the secret from travelling.
115
+ const SECRET_PATTERNS = [
116
+ { label: 'jwt', regex: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g },
117
+ { label: 'api key', regex: /\b(?:sk-[A-Za-z0-9_-]{16,}|ghp_[A-Za-z0-9]{16,}|gho_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16})/g },
118
+ { label: 'private key', regex: /-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g },
119
+ { label: 'database url with password', regex: /\b(?:postgres(?:ql)?|mysql):\/\/[^\s:/@]+:[^\s/@]{3,}@\S+/g },
120
+ { label: 'inline credential', regex: /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*(\S{12,})/gi },
121
+ ];
122
+
123
+ // Docs are full of examples. A value that is obviously a stand-in is not a leak.
124
+ const SECRET_PLACEHOLDER = /^(?:<|\{|\$|\.\.\.|x{6,}|\*{4,}|-{4,}|your[-_ ]|my[-_]|some[-_]|placeholder|example|redacted|changeme|dummy|fake|test[-_]|null|true|false|undefined)/i;
125
+
126
+ // The generic `token = ...` rule fires on code samples far more often than on
127
+ // real leaks, so a hit must also look like an opaque credential: one run of
128
+ // credential characters, not an expression, an env lookup, or a CONSTANT_NAME.
129
+ const CREDENTIAL_VALUE = /^[A-Za-z0-9_\-.+/=:~]{16,}$/;
130
+ const CREDENTIAL_REFERENCE = /process\.env|os\.environ|os\.getenv|getenv|ENV\[|env\.|config\.|settings\.|self\.|this\.|_el\./i;
131
+ const CONSTANT_NAME = /^[A-Z][A-Z0-9_]*$/;
132
+ const FILE_PATH_VALUE = /^[~/]|^\.{1,2}\//;
133
+ const ENV_ASSIGNMENT = /=(?!=*$)/;
134
+
12
135
  function showHelp() {
13
136
  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]');
137
+ console.log(' atris pack publish [--dir atris] [--slug <slug>] [--author "<name>"] [--notes "..."] [--minor|--major] [--out <file.zip>] [--push] [--dry-run] [--allow-secrets]');
15
138
  console.log(' atris pack install <file.zip|url|slug> [--dir <target>] [--force]');
139
+ console.log(' atris pack run <slug|dir> [--dir <target>] [--cloud] [--force] [--trust]');
140
+ console.log(' atris pack share <slug> [--for "<Name>"]');
16
141
  console.log(' atris pack pull [<slug>] [--dir <path>]');
17
142
  console.log(' atris pack status [--dir <path>]');
18
143
  console.log(' atris pack update [<dir>]');
@@ -129,81 +254,190 @@ function buildManifest(existing, options) {
129
254
  return manifest;
130
255
  }
131
256
 
132
- function shouldSkipRelative(relativePath, includeLogs) {
133
- const normalized = relativePath.replace(/\\/g, '/');
257
+ function packetDirectories(includeLogs) {
258
+ return includeLogs ? [...PACKET.directories, 'logs', 'journal'] : PACKET.directories;
259
+ }
260
+
261
+ function packetDeniedSegments(includeLogs) {
262
+ if (!includeLogs) return PACKET.deniedSegments;
263
+ return PACKET.deniedSegments.filter((segment) => segment !== 'logs' && segment !== 'journal');
264
+ }
265
+
266
+ // Definitions travel, running state stays home. Judged on the filename alone
267
+ // so the rule holds in any workspace, whatever its folders are called.
268
+ function isRunningStateName(lowerBase) {
269
+ if (PACKET.runningStateNames.includes(lowerBase)) return true;
270
+ return PACKET.runningStatePatterns.some((pattern) => pattern.test(lowerBase));
271
+ }
272
+
273
+ // Paths are judged relative to the pack root, so `atris/` is transparent: the
274
+ // same table decides `wiki/page.md` in a crafted pack and `atris/wiki/page.md`
275
+ // in a workspace publish.
276
+ function classifyPacketPath(relativePath, { includeLogs = false, isDirectory = false } = {}) {
277
+ const normalized = String(relativePath).replace(/\\/g, '/');
134
278
  const parts = normalized.split('/').filter(Boolean);
135
279
  const basename = parts[parts.length - 1] || '';
136
280
  const lowerBase = basename.toLowerCase();
281
+ const denied = packetDeniedSegments(includeLogs);
137
282
 
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;
283
+ for (const part of parts) {
284
+ if (denied.includes(part)) {
285
+ const reason = PACKET.segmentReasons[part];
286
+ return { ok: false, reason: reason || `runtime exhaust or dependency (${part}/)` };
287
+ }
288
+ }
289
+ if (PACKET.deniedNames.includes(lowerBase)) return { ok: false, reason: 'lockfile or editor junk' };
290
+ if (PACKET.deniedPatterns.some((pattern) => pattern.test(basename))) {
291
+ return { ok: false, reason: 'secret-shaped filename' };
292
+ }
293
+ if (!isDirectory && isRunningStateName(lowerBase)) {
294
+ return { ok: false, reason: PACKET.runningStateReason };
295
+ }
296
+
297
+ const inner = parts[0] === 'atris' ? parts.slice(1) : parts;
298
+ if (!inner.length) return { ok: true };
299
+ if (isDirectory) {
300
+ if (inner.length > 1) return { ok: true };
301
+ return packetDirectories(includeLogs).includes(inner[0])
302
+ ? { ok: true }
303
+ : { ok: false, reason: `not in the packet allowlist (${inner[0]}/)` };
141
304
  }
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;
305
+
306
+ const extension = path.extname(lowerBase);
307
+ if (inner.length === 1) {
308
+ if (PACKET.rootFiles.includes(lowerBase)) return { ok: true };
309
+ return PACKET.rootExtensions.includes(extension)
310
+ ? { ok: true }
311
+ : { ok: false, reason: 'not a root document (.md/.txt)' };
312
+ }
313
+ if (!packetDirectories(includeLogs).includes(inner[0])) {
314
+ return { ok: false, reason: `not in the packet allowlist (${inner[0]}/)` };
315
+ }
316
+ return PACKET.extensions.includes(extension)
317
+ ? { ok: true }
318
+ : { ok: false, reason: `not a text file type (${extension || 'no extension'})` };
319
+ }
320
+
321
+ // The registry rejects non-UTF-8 members, and a stranger cannot read a binary
322
+ // anyway, so prove it decodes before shipping it.
323
+ function isUtf8Text(buffer) {
324
+ if (buffer.includes(0)) return false;
325
+ return Buffer.compare(Buffer.from(buffer.toString('utf8'), 'utf8'), buffer) === 0;
149
326
  }
150
327
 
151
- function collectAtrisEntries(sourceDir, includeLogs) {
328
+ // A crafted pack keeps wiki/, README.md, and friends at the pack root, so a
329
+ // root publish walks the whole folder, not just atris/ (2026-07-09: the first
330
+ // dogfooded pack shipped without its research).
331
+ function collectPacketEntries(sourceDir, { prefix = '', includeLogs = false } = {}) {
152
332
  const entries = [];
333
+ const skipped = [];
334
+
335
+ function record(relative, reason) {
336
+ skipped.push({ path: prefix ? `${prefix}/${relative}` : relative, reason });
337
+ }
153
338
 
154
339
  function walk(dir, relativeDir = '') {
155
340
  const names = fs.readdirSync(dir).sort();
156
341
  for (const name of names) {
157
342
  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);
343
+ const rel = (relativeDir ? `${relativeDir}/${name}` : name).replace(/\\/g, '/');
344
+ const scoped = prefix ? `${prefix}/${rel}` : rel;
345
+ let stat;
346
+ try {
347
+ stat = fs.statSync(abs);
348
+ } catch {
349
+ record(rel, 'unreadable');
350
+ continue;
351
+ }
161
352
  if (stat.isDirectory()) {
353
+ const verdict = classifyPacketPath(scoped, { includeLogs, isDirectory: true });
354
+ if (!verdict.ok) {
355
+ record(`${rel}/`, verdict.reason);
356
+ continue;
357
+ }
162
358
  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
- });
359
+ continue;
360
+ }
361
+ if (!stat.isFile()) continue;
362
+ const verdict = classifyPacketPath(scoped, { includeLogs });
363
+ if (!verdict.ok) {
364
+ record(rel, verdict.reason);
365
+ continue;
366
+ }
367
+ const data = fs.readFileSync(abs);
368
+ if (!isUtf8Text(data)) {
369
+ record(rel, 'not valid utf-8 text');
370
+ continue;
169
371
  }
372
+ entries.push({ name: scoped, data, mtime: stat.mtime });
170
373
  }
171
374
  }
172
375
 
173
376
  walk(sourceDir);
174
- return entries;
377
+ return { entries, skipped };
175
378
  }
176
379
 
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 = [];
380
+ function redactSecret(value) {
381
+ const text = String(value || '');
382
+ if (text.length <= 8) return '*'.repeat(Math.max(text.length, 3));
383
+ const masked = '*'.repeat(Math.min(12, text.length - 6));
384
+ return `${text.slice(0, 4)}${masked}${text.slice(-2)}`;
385
+ }
182
386
 
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 });
387
+ function trimSecretValue(value) {
388
+ return String(value || '')
389
+ .replace(/^[`'"]+/, '')
390
+ .replace(/[`'"]+$/, '')
391
+ .replace(/[,;.)\]}>|]+$/, '');
392
+ }
393
+
394
+ function looksLikePlaceholder(value) {
395
+ return SECRET_PLACEHOLDER.test(trimSecretValue(value));
396
+ }
397
+
398
+ function looksLikeCredentialValue(value) {
399
+ const trimmed = trimSecretValue(value);
400
+ if (!CREDENTIAL_VALUE.test(trimmed)) return false;
401
+ if (CREDENTIAL_REFERENCE.test(trimmed)) return false;
402
+ if (CONSTANT_NAME.test(trimmed)) return false;
403
+ if (FILE_PATH_VALUE.test(trimmed)) return false;
404
+ if (ENV_ASSIGNMENT.test(trimmed)) return false;
405
+ // Real credentials carry entropy: a digit, or at least mixed case.
406
+ return /\d/.test(trimmed) || (/[a-z]/.test(trimmed) && /[A-Z]/.test(trimmed));
407
+ }
408
+
409
+ function scanTextForSecrets(text) {
410
+ const findings = [];
411
+ const lines = String(text).split(/\r?\n/);
412
+ for (let i = 0; i < lines.length; i += 1) {
413
+ const line = lines[i];
414
+ for (const pattern of SECRET_PATTERNS) {
415
+ pattern.regex.lastIndex = 0;
416
+ let match = pattern.regex.exec(line);
417
+ while (match) {
418
+ const value = match[1] !== undefined ? match[1] : match[0];
419
+ // The shaped patterns (jwt, sk-, AKIA, BEGIN PRIVATE KEY) are specific
420
+ // enough to stand on their own; only the generic rule needs filtering.
421
+ const generic = match[1] !== undefined;
422
+ if (!generic || (!looksLikePlaceholder(value) && looksLikeCredentialValue(value))) {
423
+ findings.push({ line: i + 1, label: pattern.label, redacted: redactSecret(trimSecretValue(value)) });
424
+ }
425
+ match = pattern.regex.exec(line);
201
426
  }
202
427
  }
203
428
  }
429
+ return findings;
430
+ }
204
431
 
205
- walk(rootDir);
206
- return entries;
432
+ function scanEntriesForSecrets(entries) {
433
+ const findings = [];
434
+ for (const entry of entries) {
435
+ if (!entry || !entry.data) continue;
436
+ for (const hit of scanTextForSecrets(entry.data.toString('utf8'))) {
437
+ findings.push({ file: entry.name, ...hit });
438
+ }
439
+ }
440
+ return findings;
207
441
  }
208
442
 
209
443
  function parseJsonBody(buffer) {
@@ -265,9 +499,7 @@ async function postPackToRegistry(manifest, zipBuffer, deps = {}) {
265
499
  const url = registryUrl('/api/pack/registry', deps);
266
500
  const body = JSON.stringify({ manifest, zipBase64: zipBuffer.toString('base64') });
267
501
  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
- }
502
+ assertPublishableAuthor(manifest);
271
503
  let response;
272
504
  try {
273
505
  response = await request(url, {
@@ -317,6 +549,173 @@ async function fetchRegistryZip(slug, deps = {}) {
317
549
  return response.body;
318
550
  }
319
551
 
552
+ function assertPublishableSlug(slug) {
553
+ if (!SLUG_RULE.test(String(slug || ''))) {
554
+ throw new Error(
555
+ `pack slug "${slug}" is not viewable on the web. use 3-40 characters of a-z, 0-9 and dashes: --slug <slug>`,
556
+ );
557
+ }
558
+ }
559
+
560
+ function assertPublishableAuthor(manifest) {
561
+ if (!manifest.author || !String(manifest.author).trim()) {
562
+ throw new Error('registry packs need an author. re-run with --author "<your name>" (or set "author" in pack.json).');
563
+ }
564
+ }
565
+
566
+ // ── pack share ──────────────────────────────────────────────────────────────
567
+ // The web renders /packs/<slug>?for=<name> and silently drops a `for` value it
568
+ // cannot display (atrisos-web app/lib/pack/personalize.ts). Mirroring that
569
+ // sanitizer here means the CLI never hands you a link that looks personalized
570
+ // and isn't — it says no instead.
571
+ const PERSONALIZATION_MAX_LENGTH = 40;
572
+ const PERSONALIZATION_MARKUP = /[<>&"`\\/]/;
573
+ const PERSONALIZATION_DISALLOWED = /[^\p{L}\p{M} '’.\-]/gu;
574
+ const PERSONALIZATION_HAS_LETTER = /\p{L}/u;
575
+
576
+ function sanitizePersonalizationName(raw) {
577
+ if (typeof raw !== 'string') return null;
578
+ if (PERSONALIZATION_MARKUP.test(raw)) return null;
579
+ const cleaned = raw
580
+ .normalize('NFC')
581
+ .replace(/\s/g, ' ')
582
+ .replace(PERSONALIZATION_DISALLOWED, '')
583
+ .replace(/ +/g, ' ')
584
+ .trim();
585
+ if (!cleaned) return null;
586
+ const capped = cleaned.slice(0, PERSONALIZATION_MAX_LENGTH).trim();
587
+ if (!capped || !PERSONALIZATION_HAS_LETTER.test(capped)) return null;
588
+ return capped;
589
+ }
590
+
591
+ function sharePack(rawArgs, cwd = process.cwd(), options = {}) {
592
+ const args = [...rawArgs];
593
+ const forName = takeValue(args, '--for');
594
+ const slug = args.shift();
595
+ if (!slug) throw new Error('pack share needs a slug: atris pack share <slug> [--for "<Name>"]');
596
+ if (args.length) throw new Error(`unknown pack share argument: ${args.join(' ')}`);
597
+ assertPublishableSlug(slug);
598
+
599
+ const deps = options.deps || {};
600
+ const base = registryUrl(`/packs/${encodeURIComponent(slug)}`, deps);
601
+ if (forName === null) {
602
+ console.log(base);
603
+ console.log(`share this packet, or personalize it: atris pack share ${slug} --for "<Name>"`);
604
+ return 0;
605
+ }
606
+
607
+ const name = sanitizePersonalizationName(forName);
608
+ if (!name) {
609
+ throw new Error(
610
+ `--for "${forName}" is not a name the share page will display, so the link would render as if it were plain. `
611
+ + `use letters, spaces and ' . - (no < > & " \` \\ /), at least one letter, up to ${PERSONALIZATION_MAX_LENGTH} characters.`,
612
+ );
613
+ }
614
+ console.log(`${base}?for=${encodeURIComponent(name)}`);
615
+ console.log(
616
+ name === forName
617
+ ? `personalized for ${name}`
618
+ : `personalized for ${name} (trimmed from "${forName}" to match what the page will show)`,
619
+ );
620
+ return 0;
621
+ }
622
+
623
+ function formatBytes(value) {
624
+ const bytes = Number(value) || 0;
625
+ if (bytes < 1024) return `${bytes} B`;
626
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
627
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
628
+ }
629
+
630
+ // What a stranger actually receives, in one glance. The tree below is the
631
+ // detail; this is the shape — enough to see that the wiki, the skills, the
632
+ // members and the policies are all still in the box.
633
+ function printPacketComposition(entries) {
634
+ const byTop = new Map();
635
+ for (const entry of entries) {
636
+ const parts = entry.name.split('/');
637
+ const inner = parts[0] === 'atris' ? parts.slice(1) : parts;
638
+ const top = inner.length > 1 ? `${inner[0]}/` : '(root)';
639
+ byTop.set(top, (byTop.get(top) || 0) + 1);
640
+ }
641
+ console.log('composition:');
642
+ for (const [top, count] of [...byTop.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))) {
643
+ console.log(` ${top.padEnd(16)} ${count}`);
644
+ }
645
+ }
646
+
647
+ function printPacketTree(entries) {
648
+ console.log('tree:');
649
+ if (entries.length <= 200) {
650
+ for (const entry of entries) console.log(` ${entry.name}`);
651
+ return;
652
+ }
653
+ const byDir = new Map();
654
+ for (const entry of entries) {
655
+ const dir = entry.name.includes('/') ? `${entry.name.slice(0, entry.name.lastIndexOf('/'))}/` : './';
656
+ const current = byDir.get(dir) || { files: 0, bytes: 0 };
657
+ current.files += 1;
658
+ current.bytes += entry.data.length;
659
+ byDir.set(dir, current);
660
+ }
661
+ console.log(` (${entries.length} files across ${byDir.size} folders, rolled up by folder)`);
662
+ for (const [dir, stats] of [...byDir.entries()].sort()) {
663
+ console.log(` ${dir} ${stats.files} files ${formatBytes(stats.bytes)}`);
664
+ }
665
+ }
666
+
667
+ function printSkipped(skipped) {
668
+ if (!skipped.length) return;
669
+ const byReason = new Map();
670
+ for (const item of skipped) {
671
+ const list = byReason.get(item.reason) || [];
672
+ list.push(item.path);
673
+ byReason.set(item.reason, list);
674
+ }
675
+ console.log('skipped:');
676
+ for (const [reason, paths] of [...byReason.entries()].sort((a, b) => b[1].length - a[1].length)) {
677
+ console.log(` ${reason}: ${paths.length}`);
678
+ for (const item of paths.slice(0, 5)) console.log(` ${item}`);
679
+ if (paths.length > 5) console.log(` ... ${paths.length - 5} more`);
680
+ }
681
+ }
682
+
683
+ function printPacketSummary(manifest, entries, skipped, zipBytes) {
684
+ const unpacked = entries.reduce((total, entry) => total + entry.data.length, 0);
685
+ console.log(`packet ${manifest.slug} ${manifest.version}`);
686
+ console.log(` files ${entries.length} (limit ${REGISTRY_LIMITS.maxEntries})`);
687
+ console.log(` unpacked ${formatBytes(unpacked)} (limit ${formatBytes(REGISTRY_LIMITS.maxUnpackedBytes)})`);
688
+ console.log(` zip ${formatBytes(zipBytes)} (limit ${formatBytes(REGISTRY_LIMITS.maxZipBytes)})`);
689
+ printPacketComposition(entries);
690
+ printPacketTree(entries);
691
+ printSkipped(skipped);
692
+ return { unpacked, zipBytes, files: entries.length };
693
+ }
694
+
695
+ function registryLimitFailures(entries, zipBytes) {
696
+ const unpacked = entries.reduce((total, entry) => total + entry.data.length, 0);
697
+ const failures = [];
698
+ if (entries.length > REGISTRY_LIMITS.maxEntries) {
699
+ failures.push(`entry count: ${entries.length} files exceeds the ${REGISTRY_LIMITS.maxEntries} file registry limit`);
700
+ }
701
+ if (unpacked > REGISTRY_LIMITS.maxUnpackedBytes) {
702
+ failures.push(`unpacked size: ${formatBytes(unpacked)} exceeds the ${formatBytes(REGISTRY_LIMITS.maxUnpackedBytes)} registry limit`);
703
+ }
704
+ if (zipBytes > REGISTRY_LIMITS.maxZipBytes) {
705
+ failures.push(`zip size: ${formatBytes(zipBytes)} exceeds the ${formatBytes(REGISTRY_LIMITS.maxZipBytes)} registry limit`);
706
+ }
707
+ return failures;
708
+ }
709
+
710
+ function reportSecretFindings(findings) {
711
+ console.error(`refusing to publish: found ${findings.length} credential-shaped match${findings.length === 1 ? '' : 'es'}.`);
712
+ for (const finding of findings.slice(0, 20)) {
713
+ console.error(` ${finding.file}:${finding.line} ${finding.label} ${finding.redacted}`);
714
+ }
715
+ if (findings.length > 20) console.error(` ... ${findings.length - 20} more`);
716
+ console.error('remove the credentials, or re-run with --allow-secrets if every match is a false positive.');
717
+ }
718
+
320
719
  async function publishPack(rawArgs, cwd = process.cwd(), options = {}) {
321
720
  const args = [...rawArgs];
322
721
  const dirArg = takeValue(args, '--dir');
@@ -327,6 +726,8 @@ async function publishPack(rawArgs, cwd = process.cwd(), options = {}) {
327
726
  const notes = takeValue(args, '--notes') || '';
328
727
  const out = takeValue(args, '--out');
329
728
  const includeLogs = takeFlag(args, '--include-logs');
729
+ const dryRun = takeFlag(args, '--dry-run');
730
+ const allowSecrets = takeFlag(args, '--allow-secrets');
330
731
  const major = takeFlag(args, '--major');
331
732
  const minor = takeFlag(args, '--minor');
332
733
  const push = takeFlag(args, '--push');
@@ -336,28 +737,68 @@ async function publishPack(rawArgs, cwd = process.cwd(), options = {}) {
336
737
  throw new Error(`pack source not found: ${path.relative(cwd, sourceDir) || sourceDir}`);
337
738
  }
338
739
 
339
- const manifestPath = packRootMode
340
- ? path.join(sourceDir, 'pack.json')
341
- : path.join(path.dirname(sourceDir), 'pack.json');
740
+ // pack.json belongs to the pack, not to whatever repo happens to contain it
741
+ // (2026-07-27: publishing ~/arena/atrisos-backend/atris wrote a pack.json
742
+ // into atrisos-backend/ and polluted the source repo).
743
+ const manifestPath = path.join(sourceDir, 'pack.json');
342
744
  const existing = readJson(manifestPath);
343
745
  const manifest = buildManifest(existing, {
344
746
  slug,
345
747
  author,
346
748
  notes,
347
749
  bump: major ? 'major' : minor ? 'minor' : 'patch',
348
- fallbackSlug: packRootMode
349
- ? path.basename(sourceDir)
350
- : path.basename(path.dirname(sourceDir)) || path.basename(sourceDir),
750
+ fallbackSlug: path.basename(sourceDir),
351
751
  });
352
- writeJson(manifestPath, manifest);
353
-
752
+ assertPublishableSlug(manifest.slug);
753
+ const shipping = Boolean(out || push);
754
+ if (shipping || dryRun) assertPublishableAuthor(manifest);
755
+ if (!dryRun) writeJson(manifestPath, manifest);
756
+
757
+ const collected = collectPacketEntries(sourceDir, {
758
+ prefix: packRootMode ? '' : 'atris',
759
+ includeLogs,
760
+ });
761
+ // The manifest is always synthesized at the zip root, never copied from the
762
+ // source, so drop whatever pack.json the walker picked up.
763
+ const sourceManifestName = packRootMode ? 'pack.json' : 'atris/pack.json';
354
764
  const entries = [
355
765
  { 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)),
766
+ ...collected.entries.filter((entry) => entry.name !== sourceManifestName),
359
767
  ];
360
- const zipBuffer = out || push ? createZipBuffer(entries) : null;
768
+
769
+ // Scan before a zip exists anywhere: on disk, in the registry, or in a temp.
770
+ if (!allowSecrets) {
771
+ const findings = scanEntriesForSecrets(entries);
772
+ if (findings.length) {
773
+ reportSecretFindings(findings);
774
+ return 1;
775
+ }
776
+ } else {
777
+ console.log('WARNING: --allow-secrets is on. Credential scanning is disabled for this publish.');
778
+ console.log('WARNING: anything you ship is readable by every person who installs this packet.');
779
+ }
780
+
781
+ const zipBuffer = createZipBuffer(entries);
782
+ printPacketSummary(manifest, entries, collected.skipped, zipBuffer.length);
783
+ const failures = registryLimitFailures(entries, zipBuffer.length);
784
+
785
+ if (dryRun) {
786
+ if (failures.length) {
787
+ console.error('dry run: this packet would be rejected by the registry.');
788
+ for (const failure of failures) console.error(` ${failure}`);
789
+ return 1;
790
+ }
791
+ console.log('dry run: nothing written. re-run without --dry-run to publish.');
792
+ return 0;
793
+ }
794
+
795
+ if (shipping && failures.length) {
796
+ console.error('refusing to publish: packet exceeds the registry limits.');
797
+ for (const failure of failures) console.error(` ${failure}`);
798
+ console.error('trim the workspace or split the packet, then re-run.');
799
+ return 1;
800
+ }
801
+
361
802
  if (out) {
362
803
  const outPath = path.resolve(cwd, out);
363
804
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
@@ -371,9 +812,13 @@ async function publishPack(rawArgs, cwd = process.cwd(), options = {}) {
371
812
  console.log(`published ${publishedSlug} ${publishedVersion}`);
372
813
  console.log(`install with: atris pack install ${publishedSlug}`);
373
814
  }
374
- if (!out && !push) {
815
+ if (!shipping) {
375
816
  console.log(`wrote pack.json for ${manifest.slug} ${manifest.version}`);
376
817
  console.log('share with: atris pack publish --out <file.zip> or atris pack publish --push');
818
+ if (failures.length) {
819
+ console.log('note: this packet is too big for the registry today:');
820
+ for (const failure of failures) console.log(` ${failure}`);
821
+ }
377
822
  if (!manifest.author || !String(manifest.author).trim()) {
378
823
  console.log('note: publishing to the registry needs an author. add --author "<your name>".');
379
824
  }
@@ -640,10 +1085,153 @@ async function installPack(rawArgs, cwd = process.cwd(), options = {}) {
640
1085
 
641
1086
  const displayTarget = path.relative(cwd, targetDir) || '.';
642
1087
  console.log(`installed ${slug} -> ${displayTarget}`);
643
- console.log(`cd ${shellQuote(displayTarget)} && claude`);
1088
+ console.log(`run it: atris pack run ${shellQuote(displayTarget)}`);
1089
+ console.log(`or open: cd ${shellQuote(displayTarget)} && claude`);
644
1090
  return 0;
645
1091
  }
646
1092
 
1093
+ // ── pack run ────────────────────────────────────────────────────────────────
1094
+ // Install is half a product: it leaves a folder and a suggestion. `pack run`
1095
+ // is the other half — packet in, running workspace out.
1096
+ //
1097
+ // It defaults to LOCAL on purpose. The cloud path is gated: activating a
1098
+ // business workspace needs auth, an existing business record, and a paid plan
1099
+ // (backend/routers/business/workspace.py `_require_owner_paid_plan`). A
1100
+ // stranger who receives a shared packet link has none of those, so the free
1101
+ // local computer is the only honest default. --cloud opts in, and when the
1102
+ // gate bites it says so instead of silently degrading.
1103
+
1104
+ function packRunLocalHint(displayTarget) {
1105
+ return `atris pack run ${shellQuote(displayTarget)}`;
1106
+ }
1107
+
1108
+ // A packet folder is a folder with a readable pack.json. Anything else is a
1109
+ // mistake worth naming.
1110
+ function assertPacketDir(dir, cwd) {
1111
+ const display = path.relative(cwd, dir) || '.';
1112
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
1113
+ throw new Error(`packet folder not found: ${display}`);
1114
+ }
1115
+ if (!fs.existsSync(path.join(dir, 'pack.json'))) {
1116
+ throw new Error(`not an atris packet (no pack.json): ${display}`);
1117
+ }
1118
+ try {
1119
+ return readPackManifestFromDir(dir);
1120
+ } catch {
1121
+ throw new Error(`packet is invalid (unreadable pack.json): ${display}`);
1122
+ }
1123
+ }
1124
+
1125
+ function looksLikeExistingDir(source, cwd) {
1126
+ if (/^https?:\/\//i.test(source)) return false;
1127
+ const resolved = path.resolve(cwd, source);
1128
+ return fs.existsSync(resolved) && fs.statSync(resolved).isDirectory();
1129
+ }
1130
+
1131
+ // The gates, checked in the order a stranger hits them, each with the free way
1132
+ // out. No fallback: a paywall you route around quietly is a paywall you lied
1133
+ // about.
1134
+ function cloudGateFailure(packDir, displayTarget, deps = {}) {
1135
+ const readCredentials = deps.loadCredentials || loadCredentials;
1136
+ const credentials = readCredentials();
1137
+ if (!credentials || !credentials.token) {
1138
+ return ['not logged in, so the cloud workspace is out of reach.', 'run: atris login'];
1139
+ }
1140
+ const readBinding = deps.readBusinessBinding || require('./computer').readBusinessBinding;
1141
+ const binding = readBinding(packDir);
1142
+ if (!binding) {
1143
+ return [
1144
+ 'no business is bound to this packet folder, so there is no cloud workspace to run in.',
1145
+ 'run: atris business init "<name>"',
1146
+ ];
1147
+ }
1148
+ return null;
1149
+ }
1150
+
1151
+ async function startPackCloud(packDir, displayTarget, deps = {}) {
1152
+ const failure = cloudGateFailure(packDir, displayTarget, deps);
1153
+ if (failure) {
1154
+ console.error(`cloud is unavailable: ${failure[0]}`);
1155
+ console.error(` ${failure[1]}`);
1156
+ console.error('cloud workspaces also need a paid plan on the owner account.');
1157
+ console.error('local runs free and needs none of that:');
1158
+ console.error(` ${packRunLocalHint(displayTarget)}`);
1159
+ return 1;
1160
+ }
1161
+ const start = deps.runComputer || require('./computer').runComputer;
1162
+ const previous = process.cwd();
1163
+ process.chdir(packDir);
1164
+ try {
1165
+ await start(['cloud']);
1166
+ } finally {
1167
+ process.chdir(previous);
1168
+ }
1169
+ return 0;
1170
+ }
1171
+
1172
+ // A packet is markdown an agent reads as instructions, and it usually came
1173
+ // from someone else. `atris console` skips permission prompts because it runs
1174
+ // on a workspace you wrote; that assumption does not survive being pointed at
1175
+ // a stranger's folder, so pack run keeps prompts on unless --trust says the
1176
+ // reader vouched for it.
1177
+ function startPackLocal(packDir, deps = {}, options = {}) {
1178
+ const trust = options.trust === true;
1179
+ const start = deps.computerLocal || require('./computer').computerLocal;
1180
+ const previous = process.cwd();
1181
+ process.chdir(packDir);
1182
+ try {
1183
+ start([], { skipPermissions: trust });
1184
+ } finally {
1185
+ process.chdir(previous);
1186
+ }
1187
+ return 0;
1188
+ }
1189
+
1190
+ async function runPack(rawArgs, cwd = process.cwd(), options = {}) {
1191
+ const args = [...rawArgs];
1192
+ const source = args.shift();
1193
+ if (!source || source === 'help' || source === '--help' || source === '-h') {
1194
+ showHelp();
1195
+ return source ? 0 : 2;
1196
+ }
1197
+ const targetArg = takeValue(args, '--dir');
1198
+ const cloud = takeFlag(args, '--cloud');
1199
+ const force = takeFlag(args, '--force');
1200
+ const trust = takeFlag(args, '--trust');
1201
+ if (args.length) throw new Error(`unknown pack run argument: ${args.join(' ')}`);
1202
+
1203
+ const deps = options.deps || {};
1204
+ const install = deps.installPack || installPack;
1205
+ let packDir;
1206
+
1207
+ if (looksLikeExistingDir(source, cwd)) {
1208
+ if (targetArg) throw new Error('--dir applies when installing a packet, not when running a folder');
1209
+ packDir = path.resolve(cwd, source);
1210
+ assertPacketDir(packDir, cwd);
1211
+ } else {
1212
+ packDir = path.resolve(cwd, targetArg || slugify(source));
1213
+ const alreadyThere = !force && fs.existsSync(path.join(packDir, 'pack.json'));
1214
+ if (alreadyThere) {
1215
+ assertPacketDir(packDir, cwd);
1216
+ console.log(`using installed packet ${path.relative(cwd, packDir) || '.'}`);
1217
+ } else {
1218
+ const installArgs = [source, '--dir', packDir];
1219
+ if (force) installArgs.push('--force');
1220
+ const code = await install(installArgs, cwd, options);
1221
+ if (code !== 0) return code;
1222
+ assertPacketDir(packDir, cwd);
1223
+ }
1224
+ }
1225
+
1226
+ const displayTarget = path.relative(cwd, packDir) || '.';
1227
+ if (cloud) return startPackCloud(packDir, displayTarget, deps);
1228
+ console.log(`starting local computer in ${displayTarget}`);
1229
+ if (!trust) {
1230
+ console.log('permission prompts are on because this packet came from somewhere else; add --trust to turn them off.');
1231
+ }
1232
+ return startPackLocal(packDir, deps, { trust });
1233
+ }
1234
+
647
1235
  async function updatePack(rawArgs, cwd = process.cwd(), options = {}) {
648
1236
  const args = [...rawArgs];
649
1237
  const packDir = args.shift() || '.';
@@ -777,6 +1365,12 @@ async function run(argv = []) {
777
1365
  showHelp();
778
1366
  return subcommand ? 0 : 2;
779
1367
  }
1368
+ // `pack <sub> --help/-h` is a help request, not a subcommand argument: show
1369
+ // usage and exit 0 instead of letting the subcommand throw "unknown argument".
1370
+ if (args.includes('--help') || args.includes('-h')) {
1371
+ showHelp();
1372
+ return 0;
1373
+ }
780
1374
  if (subcommand === 'craft') {
781
1375
  const result = craftPack(args);
782
1376
  if (result && result.needsHelp) {
@@ -787,6 +1381,8 @@ async function run(argv = []) {
787
1381
  }
788
1382
  if (subcommand === 'publish') return await publishPack(args);
789
1383
  if (subcommand === 'install') return await installPack(args);
1384
+ if (subcommand === 'run') return await runPack(args);
1385
+ if (subcommand === 'share') return sharePack(args);
790
1386
  if (subcommand === 'pull') return await pullPack(args);
791
1387
  if (subcommand === 'status') return statusPack(args);
792
1388
  if (subcommand === 'update') return await updatePack(args);
@@ -803,21 +1399,18 @@ async function run(argv = []) {
803
1399
  module.exports = {
804
1400
  run,
805
1401
  craftPack,
806
- publishPack,
807
1402
  installPack,
1403
+ runPack,
1404
+ sanitizePersonalizationName,
808
1405
  pullPack,
809
- statusPack,
810
1406
  updatePack,
811
1407
  listInstalledPacks,
812
- loadZipPayload,
813
1408
  buildManifest,
814
1409
  comparePackVersions,
815
- readPackManifestFromDir,
816
- shouldSkipRelative,
817
- resolveEntryTarget,
818
- manifestVersion,
819
- originForPayload,
820
- stampOriginInManifest,
821
- assertForceInstallAllowed,
822
- writeZipEntries,
1410
+ classifyPacketPath,
1411
+ collectPacketEntries,
1412
+ scanTextForSecrets,
1413
+ redactSecret,
1414
+ registryLimitFailures,
1415
+ REGISTRY_LIMITS,
823
1416
  };