
Sealos S3
- 11 installs
- 1 repo stars
- Updated June 18, 2026
- zjy365/sealos-skills
Provisions and operates Sealos S3-compatible object storage via sealos-cli for uploads, assets, backups, presigned URLs, and bucket policy management.
About
Identifies an app's object-storage need, creates or reuses a bucket, initializes credentials, and wires the smallest safe set of env vars, then verifies the upload/download path. A developer uses it to add S3-compatible storage or replace local MinIO in development.
- Buckets default to private; making them public requires confirmation
- Never prints secret keys and won't commit S3 credentials or auth files
Sealos S3 by the numbers
- 11 all-time installs (skills.sh)
- Ranked #841 of 1,039 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/zjy365/sealos-skills --skill sealos-s3Add your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 11 |
|---|---|
| repo stars | ★ 1 |
| Last updated | June 18, 2026 |
| Repository | zjy365/sealos-skills ↗ |
What it does
Provisions and operates Sealos S3-compatible object storage via sealos-cli for uploads, assets, backups, presigned URLs, and bucket policy management.
Files
Sealos S3
Use this skill to give a project real Sealos object storage through sealos-cli s3. The default outcome is: identify the app's object-storage need, create or reuse a bucket, initialize credentials only when needed, wire the smallest safe set of local env vars, and verify the project's upload/download or presigned URL path.
This skill is grounded in zjy365/sealos-cli#28, which registered the s3 command and implemented bucket CRD operations plus S3-compatible object operations.
Safety Rules
1. Never print secret keys, full S3 credential blocks, or copied env values in the final answer. 2. Do not overwrite an existing env value without confirming or preserving the old value. 3. Do not commit .env, .env.local, S3 access keys, secret keys, kubeconfig, or Sealos auth files. 4. Ask before making a bucket public. Default bucket policy is private. 5. Ask before destructive operations: s3 delete-bucket, s3 delete, credential rotation for an active app, or replacing app storage configuration. 6. Use JSON output from sealos-cli by default and parse it instead of scraping table output. 7. Treat s3 secret output as sensitive even though the CLI can print it.
Workflow
1. Resolve the target project
Confirm the working directory with pwd or git rev-parse --show-toplevel.
Run the analyzer when a project directory is available:
node <SKILL_DIR>/scripts/analyze-project-s3.mjs <project-dir>Use the analyzer result as a starting point, then inspect the real files it cites before editing anything. It intentionally avoids printing secret values.
2. Check sealos-cli
Prefer an existing sealos-cli binary:
sealos-cli --version
sealos-cli s3 --help
sealos-cli whoamiIf it is not installed, use npx -y sealos-cli@latest ... for one-off commands. Ask before installing it globally.
If auth is missing or expired, run:
sealos-cli login <region>
sealos-cli workspace list
sealos-cli workspace currentUse the workspace the user expects. If multiple workspaces exist and the target is ambiguous, ask before provisioning. sealos-cli s3 derives the object-storage user from the active kubeconfig namespace, so a wrong workspace means wrong buckets and credentials.
3. Choose create or reuse
List existing buckets first:
sealos-cli s3 buckets -o jsonReuse an existing bucket when its purpose and policy match. Create a new one when the project has no suitable bucket or the user asks for a fresh bucket:
sealos-cli s3 create-bucket <bucket-name> --policy private -o jsonUse private unless the user explicitly needs public reads or writes. Bucket policies accepted by the PR are private, publicRead, and publicReadwrite; aliases such as public-read normalize to publicRead, but use canonical values in instructions and scripts.
4. Initialize credentials only when needed
For app env wiring or object operations, fetch credentials:
sealos-cli s3 secret -o jsonThe command creates the ObjectStorageUser if it does not exist, then waits briefly for status. If credentials are not ready, retry after a few seconds instead of creating raw CRDs by hand.
Use references/sealos-cli-s3.md for the current command contract and response handling.
5. Wire the development environment
Map only the keys the project already uses. Common targets:
| Project signal | Preferred env keys |
|---|---|
| AWS SDK / S3 generic | S3_ENDPOINT, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BUCKET |
| AWS-style config | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, S3_BUCKET |
| MinIO replacement | existing MINIO_* keys or migrate to existing S3 keys only if the app supports them |
| Upload libraries | the keys read by the adapter/config file |
Use endpoint from secret.external for local laptop development. Use secret.internal only when the app runs inside Sealos/Devbox and the runtime can reach the internal endpoint.
Read references/env-integration.md before editing env files.
6. Verify application storage behavior
Run the smallest real project path that proves object storage works:
1. Run a repo script or test that uploads and reads an object if available. 2. Otherwise upload a small local test file with sealos-cli s3 upload, list it, download it to a temp path, and delete the test object. 3. For presigned URL features, run sealos-cli s3 presign <bucket> <key> --expires 3600 -o json and verify the URL only when that is part of the requested workflow.
Use --endpoint, --access-key, and --secret-key together only when connecting to a non-Sealos S3-compatible endpoint. Do not mix partial overrides.
7. Report the result
Summarize:
1. Bucket name, policy, region/workspace, and readiness. 2. Env file and keys updated, without revealing secret values. 3. Verification command and outcome. 4. Any public policy, credential rotation, or cleanup follow-up.
Common Tasks
Connect an existing project to Sealos object storage
1. Run the analyzer. 2. Inspect the env/config files it cites. 3. List existing buckets. 4. Create or reuse the matching bucket. 5. Fetch credentials with s3 secret. 6. Write only the env keys the app reads. 7. Verify the app's storage path.
Replace local MinIO for development
1. Identify the app service env vars that point at MinIO or an S3-compatible service. 2. Create or reuse a private Sealos bucket. 3. Update only the app's local env file, not the compose file, unless the user asks to remove MinIO. 4. Keep local Compose rollback simple: the original MinIO service remains available.
Upload or share project assets
1. Confirm the target bucket and object key prefix. 2. Upload with sealos-cli s3 upload <bucket> <file> --key <key> -o json. 3. Use presign for temporary sharing instead of public bucket policy when possible. 4. Delete temporary test objects after verification.
References
scripts/analyze-project-s3.mjs- read-only project object-storage intent analyzer.references/sealos-cli-s3.md- PR #28sealos-cli s3command contract.references/env-integration.md- safe env-file editing and S3 env-key mapping.
interface:
display_name: "Sealos: S3 Object Storage"
short_description: "Use Sealos S3 object storage in projects."
default_prompt: "Use $sealos-s3 to create or connect Sealos object storage for this project."
{
"skill_name": "sealos-s3",
"evals": [
{
"id": 0,
"prompt": "/sealos-s3 create private object storage for this repo and wire S3 env vars",
"expected_output": "Analyzes the project, checks sealos-cli auth/workspace, lists existing buckets, creates or reuses a private bucket, fetches S3 credentials without printing secrets, writes only the local S3 env keys the app reads, and verifies the storage path.",
"files": [],
"assertions": [
{
"name": "uses-analyzer",
"description": "Runs scripts/analyze-project-s3.mjs or performs equivalent file-backed detection before choosing env keys"
},
{
"name": "uses-sealos-cli-s3",
"description": "Uses sealos-cli s3 commands from PR #28 instead of raw kubectl objectstorage CRDs"
},
{
"name": "protects-secrets",
"description": "Does not print access keys, secret keys, or full credential blocks in the final answer"
}
]
},
{
"id": 1,
"prompt": "/sealos-s3 replace my local MinIO bucket with Sealos object storage for development",
"expected_output": "Detects MinIO/S3 env usage, keeps Docker Compose rollback intact unless asked otherwise, creates or reuses a private Sealos bucket, maps Sealos credentials into the existing MinIO/S3-compatible env keys, and verifies upload/download behavior.",
"files": [],
"assertions": [
{
"name": "preserves-rollback",
"description": "Does not remove or rewrite the local MinIO service unless the user explicitly asks"
},
{
"name": "maps-existing-keys",
"description": "Uses existing MINIO_* or S3_* keys read by the app rather than inventing unused aliases"
}
]
},
{
"id": 2,
"prompt": "/sealos-s3 make uploaded images publicly readable",
"expected_output": "Explains bucket policy choices, asks before changing a bucket to publicRead or publicReadwrite, and suggests presigned URLs when temporary sharing is enough.",
"files": [],
"assertions": [
{
"name": "asks-before-public",
"description": "Does not run update-bucket --policy publicRead/publicReadwrite without user confirmation"
},
{
"name": "mentions-presign",
"description": "Recommends sealos-cli s3 presign for temporary sharing when appropriate"
}
]
},
{
"id": 3,
"prompt": "/sealos-s3 rotate credentials for this app",
"expected_output": "Warns that rotation affects active clients, asks for confirmation, runs rotate-secret only after confirmation, polls secret until the new version is ready, updates dependent local env values without exposing them, and verifies the app can still access storage.",
"files": [],
"assertions": [
{
"name": "rotation-is-confirmed",
"description": "Treats rotate-secret as sensitive and asks before rotating credentials for an active app"
},
{
"name": "handles-async-status",
"description": "Recognizes rotate-secret returns status updating and polls s3 secret before updating apps"
}
]
}
]
}
S3 Environment Integration
Use this reference when wiring Sealos object storage credentials into a development project.
File Choice
Prefer the project's existing convention:
1. .env.local for Next.js and similar local-only app config. 2. .env when the repo already uses it for local development and it is ignored by git. 3. Framework-specific files such as .dev.vars, .env.development, or apps/*/.env.local when the code already reads them. 4. .env.example only for placeholder documentation. Never write real secrets into example files.
Before writing secrets, verify the file is ignored:
git check-ignore .env .env.local .env.development .dev.varsIf the target file is tracked or not ignored, stop and choose an ignored local env file instead.
Env Key Mapping
Prefer keys already used by the app.
| App style | Common keys |
|---|---|
| Generic S3 | S3_ENDPOINT, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_BUCKET, S3_REGION |
| AWS SDK | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_ENDPOINT_URL_S3, S3_BUCKET |
| MinIO-compatible | MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MINIO_BUCKET |
| Rails Active Storage S3 | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_BUCKET, AWS_ENDPOINT |
| Laravel filesystem S3 | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, AWS_BUCKET, AWS_ENDPOINT, AWS_USE_PATH_STYLE_ENDPOINT |
| Django storages | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_STORAGE_BUCKET_NAME, AWS_S3_ENDPOINT_URL, AWS_S3_REGION_NAME |
If multiple keys exist, update the one read by the runtime entry point or storage adapter config. Do not create extra aliases unless the app needs them.
Values From sealos-cli s3 secret
Map fields conservatively:
secret.CONSOLE_ACCESS_KEY -> access key env
secret.CONSOLE_SECRET_KEY -> secret key env
secret.external -> local laptop endpoint
secret.internal -> Sealos/Devbox internal endpoint
bucket name -> bucket env
region -> us-east-1
path style -> true, when the app exposes such a settingsealos-cli s3 itself uses the AWS SDK with forcePathStyle: true and region us-east-1. Prefer the same settings when a project asks for region or path-style options.
Editing Rules
1. Preserve comments, blank lines, and unrelated keys. 2. Replace only the selected keys. 3. If a key exists and has a non-empty value, preserve the old value in chat as "replaced existing local value" without printing it. 4. Quote values only if the project's env files already use quotes or the value contains characters that the loader requires quoted. 5. Never print the full resulting credential set in the final answer. 6. Do not write CONSOLE_ACCESS_KEY and CONSOLE_SECRET_KEY unless the project already expects those exact names; they are CLI response field names, not generally app env names.
Bucket and Endpoint Choices
Use a private bucket for app uploads unless the user explicitly wants public reads. Prefer presigned URLs for temporary sharing.
Use secret.external for local-machine development and secret.internal for apps running inside Sealos/Devbox when that runtime can reach the internal endpoint.
Do not switch a project from local MinIO to Sealos by editing Docker Compose unless the user asks. Updating local env keeps rollback easy.
Verification
Use the project's own path when available:
- Next.js / Node: run the upload route test, storage service test, or smallest script that calls the configured S3 client.
- Rails: run the Active Storage smoke path or a targeted storage test.
- Django: run a storage backend smoke test or targeted test.
- Generic app: upload a small object, list it, download it to a temp path, then delete the test object.
Fallback CLI smoke test:
printf 'sealos-s3-smoke\n' > /tmp/sealos-s3-smoke.txt
sealos-cli s3 upload <bucket> /tmp/sealos-s3-smoke.txt --key smoke/sealos-s3-smoke.txt -o json
sealos-cli s3 list <bucket> --prefix smoke/ -o json
sealos-cli s3 download <bucket> smoke/sealos-s3-smoke.txt /tmp/sealos-s3-smoke.out -o json
sealos-cli s3 delete <bucket> smoke/sealos-s3-smoke.txt -o jsonDo not leave temporary objects behind after verification.
sealos-cli S3 Reference
Use sealos-cli s3 as the execution layer for Sealos object storage work. This reference is based on zjy365/sealos-cli#28 (feat: add s3 object storage commands), which registered s3 in src/main.ts and implemented it in src/commands/s3/index.ts.
Install and Auth
Prefer an existing binary:
sealos-cli --version
sealos-cli whoami
sealos-cli s3 --helpUse one-off execution when the binary is missing:
npx -y sealos-cli@latest s3 --helpAuthenticate and choose the workspace:
sealos-cli login https://usw-1.sealos.io
sealos-cli workspace list
sealos-cli workspace switch <workspace-id-or-team-name>
sealos-cli workspace currentsealos-cli s3 reads the active kubeconfig and uses the current context namespace. If the context has no namespace, the CLI tells the user to run sealos-cli login or sealos-cli workspace switch.
Implementation Model
Bucket and credential commands talk to Kubernetes custom resources in the active namespace:
ObjectStorageBucketin API groupobjectstorage.sealos.io/v1ObjectStorageUserin API groupobjectstorage.sealos.io/v1
The object storage user name is derived from the namespace by removing a leading ns-. For example, namespace ns-private maps to user private.
Object operations use the AWS SDK S3 client against the S3-compatible endpoint returned in ObjectStorageUser.status.external, with forcePathStyle: true and region us-east-1.
Bucket Names and Policies
Bucket CR names are stored without the workspace prefix. Formatted bucket names include the namespace-derived prefix:
namespace ns-private + CR name assets -> bucket name private-assetsThe CLI accepts either the displayed bucket name or the CR name for bucket CRD operations and normalizes away the namespace prefix.
Supported canonical policies:
privatepublicReadpublicReadwrite
Accepted aliases include readonly, read, public-read, readwrite, read-write, public-readwrite, and public-read-write. Do not use public; the PR tests require it to fail.
Bucket Commands
Use JSON for automation:
sealos-cli s3 buckets -o json
sealos-cli s3 create-bucket <name> --policy private -o json
sealos-cli s3 get-bucket <name> -o json
sealos-cli s3 update-bucket <name> --policy publicRead -o json
sealos-cli s3 delete-bucket <name> -o jsonAliases:
sealos-cli s3 list-buckets -o json
sealos-cli s3 rm-bucket <name> -o jsoncreate-bucket updates the policy if the bucket already exists. Ask before changing a bucket from private to publicRead or publicReadwrite.
Formatted bucket output includes:
{
"name": "private-assets",
"crName": "assets",
"policy": "private",
"isComplete": true,
"createdAt": "2026-05-27T00:00:00Z",
"uid": "bucket-uid"
}buckets -o json wraps the list as { "list": [...] }.
Credentials and Quota
Initialize or read credentials:
sealos-cli s3 secret -o jsonJSON shape:
{
"secret": {
"CONSOLE_ACCESS_KEY": "<access-key>",
"CONSOLE_SECRET_KEY": "<secret-key>",
"internal": "<internal-endpoint>",
"external": "<external-endpoint>",
"specVersion": 0,
"version": 0
}
}Treat every field in secret as sensitive except endpoint hostnames. Do not print the access key or secret key in final answers.
Rotate credentials:
sealos-cli s3 rotate-secret -o jsonThe response is asynchronous:
{
"success": true,
"action": "rotate-secret",
"resource": "s3-user",
"name": "<object-storage-user>",
"specVersion": 123456789,
"status": "updating"
}After rotation, rerun sealos-cli s3 secret -o json and update dependent env values only after the new status is ready.
Check quota:
sealos-cli s3 quota -o jsonResponse shape:
{
"quota": {
"total": 10737418240,
"used": 1024,
"count": 1
}
}Fields can be null if status is not populated.
Object Commands
Object commands initialize Sealos credentials automatically unless all three overrides are provided:
--endpoint <url>
--access-key <key>
--secret-key <key>Provide all three override flags together or none.
List objects:
sealos-cli s3 list <bucket> --prefix images/ --delimiter / --max-keys 100 --token <continuation-token> -o json--max-keys must be a positive integer. JSON output:
{
"prefixes": ["images/"],
"objects": [
{
"key": "images/logo.png",
"size": 1234,
"lastModified": "2026-05-27T00:00:00.000Z",
"eTag": "\"etag\"",
"storageClass": null
}
],
"isTruncated": false,
"nextContinuationToken": null
}Upload:
sealos-cli s3 upload <bucket> <file> --key <object-key> --content-type image/png -o jsonIf --key is omitted, the CLI uses the local file path as the object key. Prefer passing an explicit key for reproducible app assets.
Download:
sealos-cli s3 download <bucket> <key> <file> -o jsonDelete an object:
sealos-cli s3 delete <bucket> <key> -o json
sealos-cli s3 rm <bucket> <key> -o jsonPresign:
sealos-cli s3 presign <bucket> <key> --expires 3600 --method get -o json
sealos-cli s3 presign <bucket> <key> --expires 3600 --method put -o json--expires must be a positive integer. --method must be get or put.
Response Handling
Every registered action command defaults -o, --output to json in PR #28. Keep using JSON for automation and table only for human inspection.
Credential initialization and rotation are asynchronous around ObjectStorageUser.status. If s3 secret says the user secret is not ready, retry after a few seconds. If rotate-secret returns status: "updating", poll s3 secret before updating applications.
PR #28 Evidence Points
The PR tests assert:
- Top-level help exposes
s3. s3 --helpdocumentscreate-bucket,rotate-secret, andpresign.- Subcommands are
buckets,create-bucket,get-bucket,update-bucket,delete-bucket,secret,rotate-secret,quota,list,upload,download,delete, andpresign. - Aliases are
list-buckets,rm-bucket, andrm. - All action commands default to JSON output.
publicis not a supported bucket policy alias.
#!/usr/bin/env node
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
import { basename, join, resolve } from 'node:path'
const root = resolve(process.argv[2] || process.cwd())
const MAX_FILE_BYTES = 1024 * 1024
const MAX_FINDINGS_PER_KIND = 60
const ignoredDirs = new Set([
'.git',
'node_modules',
'dist',
'build',
'.next',
'.turbo',
'.venv',
'venv',
'__pycache__',
'coverage',
'.sealos'
])
const textFilePatterns = [
/^package\.json$/,
/^pnpm-lock\.yaml$/,
/^package-lock\.json$/,
/^yarn\.lock$/,
/^bun\.lockb?$/,
/^docker-compose.*\.ya?ml$/,
/^compose.*\.ya?ml$/,
/^\.env.*$/,
/^.*\.env$/,
/^.*\.(ts|tsx|js|jsx|mjs|cjs|py|rb|go|rs|java|kt|php|yaml|yml|json|toml|env)$/
]
const signals = [
{
type: 's3-sdk',
confidence: 5,
patterns: [
/@aws-sdk\/client-s3/i,
/\baws-sdk\b/i,
/\bboto3\b/i,
/\bbotocore\b/i,
/\bgithub\.com\/aws\/aws-sdk-go/i,
/\bsoftware\.amazon\.awssdk/i,
/\bAws\\S3\\S3Client\b/i
]
},
{
type: 's3-env',
confidence: 5,
patterns: [
/\bS3_(ENDPOINT|BUCKET|ACCESS_KEY|ACCESS_KEY_ID|SECRET_ACCESS_KEY|REGION)\b/i,
/\bAWS_(ACCESS_KEY_ID|SECRET_ACCESS_KEY|REGION|ENDPOINT|BUCKET)\b/i,
/\bAWS_ENDPOINT_URL_S3\b/i
]
},
{
type: 'minio',
confidence: 4,
patterns: [
/\bMINIO_(ENDPOINT|ACCESS_KEY|SECRET_KEY|BUCKET|ROOT_USER|ROOT_PASSWORD)\b/i,
/\bminio\b/i,
/minio\/minio/i,
/play\.min\.io/i
]
},
{
type: 'object-storage-code',
confidence: 3,
patterns: [
/\bPutObject(Command)?\b/i,
/\bGetObject(Command)?\b/i,
/\bListObjects(V2)?(Command)?\b/i,
/\bDeleteObject(Command)?\b/i,
/\bgetSignedUrl\b/i,
/\bpresigned?\b/i,
/\bupload(File|Object)?\b/i,
/\bobject\s*storage\b/i
]
},
{
type: 'framework-storage',
confidence: 3,
patterns: [
/\bactive_storage\b/i,
/\bdjango-storages\b/i,
/\bstorages\.backends\.s3\b/i,
/\bfilesystem\s*=>\s*['"]s3['"]/i,
/\bmulter-s3\b/i,
/\bnext-s3-upload\b/i
]
}
]
const envKeyPattern = /^(S3_|AWS_|MINIO_|OBJECT_STORAGE_|STORAGE_).+/i
function isTextCandidate (filePath) {
const name = basename(filePath)
return textFilePatterns.some((pattern) => pattern.test(name))
}
function walk (dir, files = []) {
let entries
try {
entries = readdirSync(dir, { withFileTypes: true })
} catch {
return files
}
for (const entry of entries) {
if (ignoredDirs.has(entry.name)) continue
const fullPath = join(dir, entry.name)
if (entry.isDirectory()) {
walk(fullPath, files)
continue
}
if (!entry.isFile()) continue
if (!isTextCandidate(fullPath)) continue
try {
if (statSync(fullPath).size > MAX_FILE_BYTES) continue
} catch {
continue
}
files.push(fullPath)
}
return files
}
function safeRead (filePath) {
try {
return readFileSync(filePath, 'utf8')
} catch {
return ''
}
}
function relative (filePath) {
return filePath.startsWith(root) ? filePath.slice(root.length + 1) : filePath
}
function addFinding (bucket, finding) {
if (bucket.length >= MAX_FINDINGS_PER_KIND) return
bucket.push(finding)
}
function fileWeight (filePath) {
const rel = relative(filePath)
const name = basename(filePath)
if (/^\.env|\.env$/.test(name) || name.includes('.env.')) return 5
if (name === 'package.json') return 4
if (/docker-compose.*\.ya?ml$|compose.*\.ya?ml$/i.test(name)) return 3
if (/(^|\/)(config|storage|upload|uploads|lib|src|app)(\/|$)/i.test(rel)) return 2
if (/(^|\/)(__tests__|test|tests|spec|specs|coverage|docs?|generated)(\/|$)/i.test(rel)) return 0.2
if (/README|CHANGELOG|LICENSE|SECURITY/i.test(name)) return 0.2
return 1
}
function scanEnvFiles (files) {
const envFiles = []
const keys = {}
for (const filePath of files) {
const name = basename(filePath)
if (!/^\.env|\.env$/.test(name) && !name.includes('.env.')) continue
const content = safeRead(filePath)
const fileKeys = []
for (const [index, line] of content.split(/\r?\n/).entries()) {
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/)
if (!match) continue
const key = match[1]
fileKeys.push(key)
if (!keys[key]) keys[key] = []
keys[key].push({ file: relative(filePath), line: index + 1 })
}
envFiles.push({ file: relative(filePath), keys: fileKeys })
}
return { envFiles, keys }
}
function scanPackageJson () {
const packagePath = join(root, 'package.json')
if (!existsSync(packagePath)) return null
try {
const pkg = JSON.parse(readFileSync(packagePath, 'utf8'))
return {
packageManager: pkg.packageManager || null,
scripts: pkg.scripts || {},
dependencies: {
...pkg.dependencies,
...pkg.devDependencies
}
}
} catch {
return null
}
}
function scoreFiles (files) {
const scores = {}
const findings = []
for (const filePath of files) {
const content = safeRead(filePath)
if (!content) continue
const weight = fileWeight(filePath)
const lines = content.split(/\r?\n/)
for (const [lineIndex, line] of lines.entries()) {
for (const signal of signals) {
for (const pattern of signal.patterns) {
if (!pattern.test(line)) continue
scores[signal.type] = (scores[signal.type] || 0) + (signal.confidence * weight)
addFinding(findings, {
type: signal.type,
file: relative(filePath),
line: lineIndex + 1,
weight,
match: pattern.source
})
break
}
}
}
}
return { scores, findings }
}
function choosePrimary (scores) {
const ranked = Object.entries(scores)
.sort((a, b) => b[1] - a[1])
.map(([type, score]) => ({ type, score: Number(score.toFixed(2)) }))
if (ranked.length === 0) return { primary: null, ranked }
const [first, second] = ranked
const confidence = !second ? 'high' : first.score >= second.score * 1.5 ? 'high' : 'medium'
return { primary: { ...first, confidence }, ranked }
}
function suggestEnvTargets (envKeys) {
const preferredGroups = [
['S3_ENDPOINT', 'S3_ACCESS_KEY_ID', 'S3_SECRET_ACCESS_KEY', 'S3_BUCKET', 'S3_REGION'],
['AWS_ENDPOINT_URL_S3', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_REGION', 'S3_BUCKET'],
['MINIO_ENDPOINT', 'MINIO_ACCESS_KEY', 'MINIO_SECRET_KEY', 'MINIO_BUCKET']
]
const existing = preferredGroups
.map((group) => group.filter((key) => envKeys[key]))
.filter((group) => group.length > 0)
.sort((a, b) => b.length - a.length)
return existing[0] || preferredGroups[0]
}
function suggestBucketName () {
const base = basename(root).toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '')
return `${base || 'app'}-assets`
}
if (!existsSync(root)) {
console.error(JSON.stringify({ ok: false, error: `Path does not exist: ${root}` }, null, 2))
process.exit(1)
}
const files = walk(root)
const { envFiles, keys: envKeys } = scanEnvFiles(files)
const packageInfo = scanPackageJson()
const { scores, findings } = scoreFiles(files)
const { primary, ranked } = choosePrimary(scores)
const suggestedEnvKeys = suggestEnvTargets(envKeys)
const output = {
ok: true,
project: root,
recommendation: {
needsObjectStorage: ranked.length > 0,
confidence: primary?.confidence || 'low',
reason: primary ? 'Detected project S3/object-storage signals.' : 'No S3-specific signal found; create object storage only if the user requested it.',
suggestedBucketName: suggestBucketName(),
suggestedPolicy: 'private',
suggestedEnvKeys,
createCommand: `sealos-cli s3 create-bucket ${suggestBucketName()} --policy private -o json`
},
existingEnv: {
files: envFiles,
s3Keys: Object.fromEntries(
Object.entries(envKeys).filter(([key]) => envKeyPattern.test(key))
)
},
package: packageInfo
? {
packageManager: packageInfo.packageManager,
storageDependencies: Object.keys(packageInfo.dependencies || {}).filter((name) =>
/(@aws-sdk\/client-s3|@aws-sdk\/s3-request-presigner|aws-sdk|multer-s3|minio|next-s3-upload|s3|uploadthing)/i.test(name)
),
storageScripts: Object.fromEntries(
Object.entries(packageInfo.scripts || {}).filter(([name, value]) =>
/(s3|storage|upload|asset|media)/i.test(`${name} ${value}`)
)
)
}
: null,
rankedSignals: ranked,
findings
}
console.log(JSON.stringify(output, null, 2))