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.
- package/README.md +2 -0
- package/atris/PERSONA.md +3 -7
- package/atris.md +10 -2
- package/ax +158 -33
- package/bin/atris.js +49 -2
- package/commands/activate.js +16 -3
- package/commands/agent-spawn.js +0 -7
- package/commands/autoland.js +92 -14
- package/commands/autopilot-front.js +0 -4
- package/commands/autopilot.js +0 -12
- package/commands/brain.js +0 -7
- package/commands/brief.js +0 -2
- package/commands/business-sync.js +0 -1
- package/commands/business.js +107 -12
- package/commands/clean.js +31 -2
- package/commands/close.js +46 -11
- package/commands/cloud.js +0 -1
- package/commands/codex-goal.js +0 -2
- package/commands/computer.js +6 -5
- package/commands/console.js +46 -9
- package/commands/deck.js +29 -8
- package/commands/dream.js +0 -7
- package/commands/drill.js +0 -1
- package/commands/engine.js +10 -18
- package/commands/experiments.js +0 -4
- package/commands/fleet-report.js +12 -4
- package/commands/game.js +261 -0
- package/commands/gm.js +10 -2
- package/commands/goal.js +0 -6
- package/commands/harvest.js +0 -1
- package/commands/improve.js +29 -8
- package/commands/init.js +18 -34
- package/commands/integrations.js +0 -7
- package/commands/land.js +71 -8
- package/commands/launchpad.js +0 -2
- package/commands/lesson.js +17 -7
- package/commands/loop-front.js +0 -2
- package/commands/loops.js +6 -5
- package/commands/meet.js +0 -2
- package/commands/member.js +68 -3
- package/commands/mission.js +359 -102
- package/commands/next.js +0 -1
- package/commands/now.js +0 -12
- package/commands/one-lap.js +8 -8
- package/commands/orb.js +0 -4
- package/commands/pack-craft.js +0 -1
- package/commands/pack.js +670 -77
- package/commands/play.js +0 -2
- package/commands/probe.js +10 -4
- package/commands/pulse.js +73 -48
- package/commands/push.js +0 -4
- package/commands/radar.js +14 -11
- package/commands/recover.js +340 -0
- package/commands/report.js +0 -1
- package/commands/router.js +319 -0
- package/commands/run-front.js +0 -2
- package/commands/search.js +0 -3
- package/commands/skill.js +7 -2
- package/commands/slop.js +225 -7
- package/commands/status.js +6 -0
- package/commands/stream.js +0 -2
- package/commands/study.js +0 -3
- package/commands/sync-checkout.js +51 -0
- package/commands/sync.js +11 -40
- package/commands/task.js +120 -38
- package/commands/teach.js +86 -0
- package/commands/unknowns.js +0 -10
- package/commands/update.js +0 -1
- package/commands/visualize.js +0 -5
- package/commands/watch.js +0 -6
- package/commands/wiki.js +41 -11
- package/commands/wish.js +0 -1
- package/commands/worktree.js +87 -14
- package/commands/xp.js +1 -6
- package/commands/youtube.js +0 -2
- package/lib/accept-verify-gate.js +122 -0
- package/lib/activity-stream.js +0 -2
- package/lib/auto-accept-certified.js +145 -2
- package/lib/autoland.js +0 -3
- package/lib/ax-auto-lane.js +68 -7
- package/lib/bench/context.js +0 -2
- package/lib/bench/report.js +0 -1
- package/lib/bench/runner.js +0 -11
- package/lib/brief-ledger.js +0 -6
- package/lib/business-simulate.js +382 -0
- package/lib/chat-log-scan.js +0 -5
- package/lib/checkout-sync.js +52 -0
- package/lib/claude-boot-block.js +48 -0
- package/lib/cloud-mission.js +0 -6
- package/lib/codex-flight.js +0 -1
- package/lib/conductor-artifacts.js +25 -0
- package/lib/context-gatherer.js +0 -1
- package/lib/deck-compose.js +0 -1
- package/lib/deck-history.js +0 -1
- package/lib/deck-layout.js +0 -5
- package/lib/deck-review.js +0 -7
- package/lib/deck-schema.js +0 -1
- package/lib/default-verifier.js +0 -2
- package/lib/engine-registry.js +16 -8
- package/lib/experiments/daily.js +0 -6
- package/lib/fleet.js +210 -27
- package/lib/functional-owner.js +0 -3
- package/lib/inspect-fields.js +0 -1
- package/lib/known-commands.js +2 -2
- package/lib/learnings.js +0 -2
- package/lib/loop-doctor.js +11 -17
- package/lib/manifest.js +0 -2
- package/lib/member-alive.js +0 -1
- package/lib/member-switches.js +0 -5
- package/lib/mission-artifact.js +0 -1
- package/lib/mission-protected-lane.js +293 -0
- package/lib/mission-room.js +0 -2
- package/lib/mission-runtime-loop.js +0 -1
- package/lib/next-moves.js +0 -18
- package/lib/official-cli-integration.js +0 -4
- package/lib/orb-context.js +6 -3
- package/lib/orb-scorecard.js +0 -1
- package/lib/outbound-send-gate.js +0 -6
- package/lib/permission-grants.js +0 -6
- package/lib/policy-lessons.js +0 -2
- package/lib/pulse.js +68 -15
- package/lib/review-integrity.js +0 -2
- package/lib/router-brain.js +0 -2
- package/lib/runner-command.js +0 -4
- package/lib/runtime-bootstrap.js +0 -1
- package/lib/scorecard.js +0 -1
- package/lib/self-drive.js +1 -1
- package/lib/state-detection.js +0 -6
- package/lib/task-db.js +0 -4
- package/lib/team-presence.js +0 -1
- package/lib/voice-gate.js +63 -2
- package/lib/wiki.js +152 -9
- package/lib/wish-audit.js +20 -6
- package/lib/wish-delegate.js +26 -20
- package/lib/wish-stats.js +0 -5
- package/lib/wish-store.js +0 -1
- package/lib/workspace-safety.js +0 -2
- package/lib/zip.js +0 -1
- 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
|
|
133
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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 ?
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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:
|
|
349
|
-
? path.basename(sourceDir)
|
|
350
|
-
: path.basename(path.dirname(sourceDir)) || path.basename(sourceDir),
|
|
750
|
+
fallbackSlug: path.basename(sourceDir),
|
|
351
751
|
});
|
|
352
|
-
|
|
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
|
-
...(
|
|
357
|
-
? collectPackRootEntries(sourceDir, includeLogs)
|
|
358
|
-
: collectAtrisEntries(sourceDir, includeLogs)),
|
|
766
|
+
...collected.entries.filter((entry) => entry.name !== sourceManifestName),
|
|
359
767
|
];
|
|
360
|
-
|
|
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 (!
|
|
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(`
|
|
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
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
assertForceInstallAllowed,
|
|
822
|
-
writeZipEntries,
|
|
1410
|
+
classifyPacketPath,
|
|
1411
|
+
collectPacketEntries,
|
|
1412
|
+
scanTextForSecrets,
|
|
1413
|
+
redactSecret,
|
|
1414
|
+
registryLimitFailures,
|
|
1415
|
+
REGISTRY_LIMITS,
|
|
823
1416
|
};
|