@petercjl/seedaudiocli 0.0.0-stage → 0.1.2

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 CHANGED
@@ -1,3 +1,70 @@
1
- # Temporary Holding Version
1
+ # @petercjl/seedaudiocli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ EvoLink Seed Audio CLI with a portable companion Skill. Requires Node.js 22+ and Python 3.9+ with SSL; no Python third-party packages are needed.
4
+
5
+ ## Install and configure
6
+
7
+ ```bash
8
+ npm install -g @petercjl/seedaudiocli
9
+ seedaudiocli setup
10
+ ```
11
+
12
+ On first use, provide the path to the configuration file supplied by your administrator. For an Agent or noninteractive terminal:
13
+
14
+ ```bash
15
+ seedaudiocli config import "/path/to/seedaudio-config.json"
16
+ seedaudiocli auth status
17
+ seedaudiocli doctor --json
18
+ ```
19
+
20
+ Copy `seedaudiocli.config.example.json`, fill `SEEDAUDIO_API_KEY`, and deliver the file privately to the authorized recipient. A `.env` file using the same field names is also accepted. The CLI parses data; it does not execute configuration as a script. The package includes only blank examples. Each recipient with the file can use the key. Per-user service keys allow independent revocation and limits.
21
+
22
+ Credentials persist outside the npm installation:
23
+
24
+ - macOS/Linux: `~/.config/seedaudiocli/seedaudiocli.env`
25
+ - Windows: `%APPDATA%/seedaudiocli/seedaudiocli.env`
26
+ - Explicit override: `SEEDAUDIO_CONFIG_DIR`
27
+
28
+ macOS/Linux files use mode 0600. Windows relies on the user's profile directory ACL; administrators should keep that directory private. Import validates local structure and does not make a paid API call. `auth status` reports local availability, not remote key validity. Environment variables override saved configuration. Network errors do not clear credentials.
29
+
30
+ An existing configuration requires `--replace`; replacement creates a recoverable sibling backup before atomic write. Backups contain credentials too. Keep them private. Package uninstall/reinstall preserves configuration and task history. Existing env configurations remain supported. Legacy `config init --import-key-file FILE` remains available for older note-format files; new distribution uses `config import`.
31
+
32
+ ## Generate and recover
33
+
34
+ ```bash
35
+ seedaudiocli generate text --prompt "Generate five seconds of instrumental piano." --dry-run
36
+ seedaudiocli generate text --prompt "Generate five seconds of instrumental piano." --wait --download ./audio
37
+ seedaudiocli task wait TASK_ID --download ./audio
38
+ ```
39
+
40
+ Actual generation can incur provider charges. Save the returned task ID and recover it instead of submitting again. Unknown submission outcomes require provider/history inspection. Reference modes and parameter limits are documented by `generate --help` and the bundled Skill.
41
+
42
+ ## Agent installation
43
+
44
+ ```bash
45
+ seedaudiocli skill source --json
46
+ seedaudiocli skill install --agent codex
47
+ seedaudiocli skill status --agent codex --json
48
+ seedaudiocli skill install --agent sealseek --root "/actual/skills-directory"
49
+ ```
50
+
51
+ SealSeek's discovery directory is explicit (`--root` or `SEALSEEK_SKILLS_DIR`). Unknown Agents and unmanaged targets are refused. Managed copies have ownership/hash records; updates back up prior contents, preserve customized UI metadata and refuse local edits. In-place synchronization supports directory handles held open by an Agent. Failed synchronization reports the recovery backup.
52
+
53
+ ## Update and rollback
54
+
55
+ ```bash
56
+ npm install -g @petercjl/seedaudiocli@latest
57
+ seedaudiocli skill update --agent codex
58
+ ```
59
+
60
+ Global installs check and install stable latest before every run and synchronize previously registered managed Skills. Use `update check` to inspect availability and `update install` to apply it immediately. For a deliberate rollback, set `SEEDAUDIO_AUTO_UPDATE=0`, install the prior exact npm version and synchronize Skills. Configuration and history remain unchanged.
61
+
62
+ Python discovery checks an explicit `SEEDAUDIO_PYTHON`, configured Agent runtime directories and platform commands. `SEEDAUDIO_RUNTIME_ROOT` can point to a managed runtime root. A missing or broken runtime produces `PYTHON_REQUIRED` with installation guidance.
63
+
64
+ ## Development and evidence
65
+
66
+ Run `npm test` for isolated CLI, configuration and lifecycle tests, and `npm pack --dry-run` to inspect published contents. Tests use synthetic keys and do not submit paid jobs. GitHub QA covers macOS, Windows and Linux package entry points. Host Skill discovery and paid audio output require separate runtime evidence; adapter status remains `implemented` until those checks are recorded. Publishing uses the committed GitHub Actions OIDC workflow after QA. Public distribution does not include credentials.
67
+
68
+ ## Automatic updates
69
+
70
+ Global npm installs check stable latest before every command, install newer releases and sync managed Skills before running the requested command once. No TTL cache is used. Offline checks keep the installed version; updates back up the package for rollback. Configuration and task history remain outside the package. Use `seedaudiocli update check` or `seedaudiocli update install`. For a deliberately pinned run set `SEEDAUDIO_AUTO_UPDATE=0`. Development checkouts are Git-managed; taobao-ai-ops owns suite component updates.
@@ -0,0 +1,6 @@
1
+ {
2
+ "agent": "codex",
3
+ "skill": "skills/seedaudiocli",
4
+ "status": "implemented",
5
+ "installation": "seedaudiocli skill install --agent codex"
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "agent": "sealseek",
3
+ "skill": "skills/seedaudiocli",
4
+ "status": "implemented",
5
+ "installation": "seedaudiocli skill install --agent sealseek --root SKILLS_DIRECTORY"
6
+ }
@@ -0,0 +1,73 @@
1
+ {
2
+ "schema_version": "1.0.0",
3
+ "package": "@petercjl/seedaudiocli",
4
+ "runtime": {
5
+ "node": ">=22",
6
+ "python": ">=3.9",
7
+ "python_override": "SEEDAUDIO_PYTHON"
8
+ },
9
+ "commands": [
10
+ "version",
11
+ "capabilities",
12
+ "doctor",
13
+ "setup",
14
+ "config import",
15
+ "auth status",
16
+ "models list",
17
+ "generate text",
18
+ "generate audio-ref",
19
+ "generate image-ref",
20
+ "task get",
21
+ "task wait",
22
+ "task url",
23
+ "task jobs",
24
+ "payload validate",
25
+ "payload submit",
26
+ "skill source",
27
+ "skill status",
28
+ "skill install",
29
+ "skill update",
30
+ "update check",
31
+ "update install"
32
+ ],
33
+ "config": {
34
+ "format": [
35
+ "json",
36
+ "env"
37
+ ],
38
+ "fields": [
39
+ "SEEDAUDIO_API_KEY",
40
+ "SEEDAUDIO_BASE_URL",
41
+ "SEEDAUDIO_MODEL",
42
+ "SEEDAUDIO_DEFAULT_FORMAT",
43
+ "SEEDAUDIO_DEFAULT_SAMPLE_RATE"
44
+ ],
45
+ "precedence": [
46
+ "environment",
47
+ "saved config",
48
+ "defaults"
49
+ ],
50
+ "import": "seedaudiocli config import FILE [--replace]",
51
+ "side_effects": "Local file write, backup on replacement; no remote call",
52
+ "missing": "CONFIG_REQUIRED",
53
+ "secrets_in_output": false
54
+ },
55
+ "update": {
56
+ "trigger": "before-every-run",
57
+ "channel": "latest",
58
+ "automatic_install": true,
59
+ "configuration": "Outside package; preserved",
60
+ "offline": "keep-current",
61
+ "source_checkout": "git-managed",
62
+ "suite_install": "suite-managed",
63
+ "opt_out": "SEEDAUDIO_AUTO_UPDATE=0",
64
+ "managed_skills": "synchronized with backups; edited content preserved"
65
+ },
66
+ "generation": {
67
+ "provider": "EvoLink",
68
+ "model": "doubao-seed-audio-1-0",
69
+ "paid": true,
70
+ "submit_retry": false,
71
+ "recovery": "Use saved task ID; unknown submit outcome requires inspection before resubmission"
72
+ }
73
+ }
package/npm/cli.mjs ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import { beforeRun, checkUpdate, installUpdate } from './update.mjs';
4
+ import path from 'node:path';
5
+ import { root, pkg, fail, findPython, runPython } from './runtime.mjs';
6
+ import { source, targetPath, status, install } from './skills.mjs';
7
+
8
+ const args = process.argv.slice(2);
9
+ const print = value => console.log(JSON.stringify(value, null, 2));
10
+ const flag = name => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
11
+ try {
12
+ if (await beforeRun(args)) { /* child completed */ }
13
+ else if (['version', '--version'].includes(args[0])) console.log(`seedaudiocli ${pkg.version}`);
14
+ else if (args[0] === 'capabilities') print(JSON.parse(fs.readFileSync(path.join(root, 'capabilities.json'), 'utf8')));
15
+ else if (args[0] === 'doctor') {
16
+ const python = findPython();
17
+ const auth = runPython(['auth', 'status'], { encoding: 'utf8', stdio: 'pipe' });
18
+ let config;
19
+ try { config = JSON.parse(auth.stdout); } catch { throw fail('RUNTIME_FAILED', 'Python CLI did not return JSON.'); }
20
+ print({ ok: auth.status === 0, version: pkg.version, python, skill_source: source, config });
21
+ process.exitCode = auth.status === 0 ? 0 : 1;
22
+ } else if (args[0] === 'skill') {
23
+ if (args[1] === 'source') print({ ok: true, source, version: pkg.version });
24
+ else {
25
+ const target = targetPath(flag('--agent') || 'codex', flag('--root'));
26
+ if (args[1] === 'status') print({ ok: true, ...status(target), record: undefined });
27
+ else if (['install', 'update'].includes(args[1])) print(install(target));
28
+ else throw fail('INVALID_COMMAND', 'Use skill source|status|install|update.');
29
+ }
30
+ } else if (args[0] === 'update') {
31
+ const check = await checkUpdate();
32
+ if (args[1] === 'install' && check.updateAvailable) print(installUpdate(check));
33
+ else print({ ...check, policy: 'before-every-run', opt_out: 'SEEDAUDIO_AUTO_UPDATE=0' });
34
+ } else if (!args.length || ['--help', '-h'].includes(args[0])) {
35
+ console.log('seedaudiocli: setup | config import FILE [--replace] | auth status | generate | task | payload\nPackage: version | capabilities --json | doctor --json | skill source/status/install/update [--agent codex|sealseek] [--root SKILLS_DIRECTORY] | update check/install\nRuntime: Node.js 22+, Python 3.9+ (SEEDAUDIO_PYTHON optional).');
36
+ process.exitCode = runPython(['--help']).status || 0;
37
+ } else {
38
+ const result = runPython(args);
39
+ process.exitCode = result.status ?? 1;
40
+ }
41
+ } catch (e) {
42
+ print({ ok: false, code: e.code || 'LOCAL_OPERATION_FAILED', error: e.code ? e.message : 'Local operation failed.' });
43
+ process.exitCode = 1;
44
+ }
@@ -0,0 +1,47 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ export const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
8
+ export const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
9
+ export const fail = (code, message) => Object.assign(new Error(message), { code });
10
+ export function pythonCandidates(env = process.env) {
11
+ if (env.SEEDAUDIO_PYTHON) return [[env.SEEDAUDIO_PYTHON]];
12
+ const result = [];
13
+ // Discover bundled Agent runtimes within declared runtime directories, never through credentials.
14
+ const bases = [env.CODEX_HOME || path.join(os.homedir(), '.codex'), env.SEALSEEK_HOME, env.SEEDAUDIO_RUNTIME_ROOT].filter(Boolean);
15
+ function visit(dir, depth) {
16
+ if (depth < 0) return;
17
+ for (const item of fs.readdirSync(dir, { withFileTypes: true, throwIfNoEntry: false }) || []) {
18
+ const file = path.join(dir, item.name);
19
+ if ((item.isFile() || item.isSymbolicLink()) && /^(python3?(\.exe)?)$/.test(item.name)) result.push([file]);
20
+ else if (item.isDirectory()) visit(file, depth - 1);
21
+ }
22
+ }
23
+ for (const base of bases) {
24
+ for (const sub of ['runtimes', 'runtime', 'dependencies', 'python']) {
25
+ try { visit(path.join(base, sub), 4); } catch { /* missing/inaccessible candidate */ }
26
+ }
27
+ }
28
+ if (process.platform === 'win32') result.push(['py', '-3'], ['python'], ['python3']);
29
+ else result.push(['python3'], ['python'], ['/opt/homebrew/bin/python3'], ['/usr/local/bin/python3']);
30
+ return result;
31
+ }
32
+ export function findPython(env = process.env) {
33
+ for (const command of pythonCandidates(env)) {
34
+ const probe = spawnSync(command[0], [...command.slice(1), '-c', 'import sys,json,ssl,urllib.request,pathlib; assert sys.version_info >= (3,9); print(json.dumps({"executable":sys.executable,"version":sys.version.split()[0]}))'], { encoding: 'utf8', timeout: 5000, env, windowsHide: true });
35
+ if (probe.status === 0) {
36
+ try { return { command, ...JSON.parse(probe.stdout.trim()) }; } catch { /* invalid runtime */ }
37
+ }
38
+ }
39
+ throw fail('PYTHON_REQUIRED', 'Install Python 3.9+ with SSL support, or set SEEDAUDIO_PYTHON to its executable. Then run seedaudiocli doctor --json.');
40
+ }
41
+ export function runPython(args, options = {}) {
42
+ const env = { ...process.env, ...options.env, PYTHONUTF8: '1', PYTHONIOENCODING: 'utf-8' };
43
+ const runtime = findPython(env);
44
+ // Resolve only the bundled implementation; user PYTHONPATH cannot shadow it.
45
+ const boot = 'import sys; sys.path.insert(0,sys.argv.pop(1)); from seedaudiocli.cli import main; sys.exit(main())';
46
+ return spawnSync(runtime.command[0], [...runtime.command.slice(1), '-c', boot, root, ...args], { stdio: 'inherit', windowsHide: true, ...options, env });
47
+ }
package/npm/skills.mjs ADDED
@@ -0,0 +1,88 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+ import { stateRoot } from './update.mjs';
6
+ import { root, pkg, fail } from './runtime.mjs';
7
+
8
+ export const source = path.join(root, 'skills', 'seedaudiocli');
9
+ const marker = '.seedaudiocli-managed.json';
10
+ const metadata = 'agents/openai.yaml';
11
+ function exists(file) { try { return fs.lstatSync(file); } catch (e) { if (e.code === 'ENOENT') return null; throw e; } }
12
+ export function inventory(dir) {
13
+ const result = {};
14
+ function walk(at, prefix = '') {
15
+ for (const entry of fs.readdirSync(at, { withFileTypes: true })) {
16
+ if (entry.name === marker) continue;
17
+ const relative = prefix + entry.name;
18
+ if (entry.isSymbolicLink()) throw fail('SKILL_UNSAFE_TARGET', 'Skill files must not contain symlinks.');
19
+ if (entry.isDirectory()) walk(path.join(at, entry.name), relative + '/');
20
+ else result[relative] = crypto.createHash('sha256').update(fs.readFileSync(path.join(at, entry.name))).digest('hex');
21
+ }
22
+ }
23
+ walk(dir);
24
+ return result;
25
+ }
26
+ export function targetPath(agent, targetRoot) {
27
+ if (!['codex', 'sealseek'].includes(agent)) throw fail('INVALID_AGENT', 'Choose --agent codex or sealseek.');
28
+ let base = targetRoot;
29
+ if (!base && agent === 'codex') base = path.join(process.env.CODEX_HOME || path.join(os.homedir(), '.codex'), 'skills');
30
+ if (!base && agent === 'sealseek') base = process.env.SEALSEEK_SKILLS_DIR;
31
+ if (!base) throw fail('AGENT_PATH_REQUIRED', 'Provide the actual SealSeek skills directory through --root or SEALSEEK_SKILLS_DIR.');
32
+ return path.join(path.resolve(base), 'seedaudiocli');
33
+ }
34
+ export function status(target) {
35
+ const st = exists(target);
36
+ if (!st) return { installed: false, managed: false, current: false, target };
37
+ if (st.isSymbolicLink() || !st.isDirectory()) return { installed: true, managed: false, current: false, target };
38
+ let record;
39
+ try { record = JSON.parse(fs.readFileSync(path.join(target, marker), 'utf8')); } catch { return { installed: true, managed: false, current: false, target }; }
40
+ if (record.package !== pkg.name || !record.files || typeof record.files !== 'object') return { installed: true, managed: false, current: false, target };
41
+ if (Object.keys(record.files).some(file => !file || path.isAbsolute(file) || file.includes('\\') || file.split('/').some(part => ['.', '..', ''].includes(part)))) throw fail('SKILL_UNSAFE_TARGET', 'Invalid managed-file path in Skill ownership record.');
42
+ const files = inventory(target);
43
+ const edited = Object.entries(record.files).some(([file, hash]) => file !== metadata && files[file] !== hash);
44
+ const expected = inventory(source);
45
+ const current = !edited && Object.entries(expected).every(([file, hash]) => file === metadata || files[file] === hash);
46
+ return { installed: true, managed: true, current, edited, target, version: record.version, record };
47
+ }
48
+ export function install(target) {
49
+ const state = status(target);
50
+ const registry = path.join(stateRoot(), 'skill-targets');
51
+ fs.mkdirSync(registry, { recursive: true });
52
+ const registration = path.join(registry, crypto.createHash('sha256').update(target).digest('hex') + '.json');
53
+ if (!exists(registration)) fs.writeFileSync(registration, JSON.stringify({ target }), { flag: 'wx', mode: 0o600 });
54
+ if (state.installed && !state.managed) throw fail('SKILL_UNMANAGED', 'An existing user-owned Skill occupies this target; preserve it and choose a new root or explicitly migrate it.');
55
+ if (state.edited) throw fail('SKILL_EDITED', 'Managed Skill files contain local edits; preserve and reconcile those edits before updating.');
56
+ if (state.current && state.version === pkg.version) return { ...state, record: undefined, ok: true, changed: false };
57
+ const expected = inventory(source);
58
+ for (const file of Object.keys(expected)) {
59
+ if (state.installed && exists(path.join(target, file)) && !(file in state.record.files) && file !== metadata) throw fail('SKILL_UNMANAGED_FILE', 'A new bundled file would overwrite a user-owned file.');
60
+ }
61
+ fs.mkdirSync(path.dirname(target), { recursive: true });
62
+ const lock = target + '.install-lock';
63
+ let lockfd;
64
+ try { lockfd = fs.openSync(lock, 'wx', 0o600); } catch (e) { if (e.code === 'EEXIST') throw fail('SKILL_BUSY', 'Another Skill installation holds the lock.'); throw e; }
65
+ let backup;
66
+ try {
67
+ if (state.installed) {
68
+ backup = path.join(stateRoot(), 'skill-backups', crypto.randomUUID());
69
+ fs.mkdirSync(path.dirname(backup), { recursive: true });
70
+ fs.cpSync(target, backup, { recursive: true, errorOnExist: true, force: false });
71
+ } else fs.mkdirSync(target);
72
+ // In-place updates support hosts holding a directory handle open. Backup is recoverable.
73
+ for (const file of Object.keys(expected)) {
74
+ if (file === metadata && exists(path.join(target, file))) continue;
75
+ fs.mkdirSync(path.dirname(path.join(target, file)), { recursive: true });
76
+ fs.copyFileSync(path.join(source, file), path.join(target, file));
77
+ }
78
+ for (const file of Object.keys(state.record?.files || {})) {
79
+ if (file !== metadata && !(file in expected)) fs.unlinkSync(path.join(target, file));
80
+ }
81
+ const actual = inventory(target);
82
+ if (Object.entries(expected).some(([file, hash]) => file !== metadata && actual[file] !== hash)) throw fail('SKILL_VERIFY_FAILED', 'Installed Skill hashes differ from the package.');
83
+ fs.writeFileSync(path.join(target, marker), JSON.stringify({ package: pkg.name, version: pkg.version, files: expected }, null, 2));
84
+ return { ok: true, installed: true, managed: true, current: true, changed: true, target, version: pkg.version, backup: backup || null };
85
+ } catch (e) {
86
+ throw fail('SKILL_INSTALL_FAILED', `Skill installation failed; ${backup ? 'restore from ' + backup : 'inspect the partial target at ' + target}.`);
87
+ } finally { fs.closeSync(lockfd); fs.unlinkSync(lock); }
88
+ }
package/npm/update.mjs ADDED
@@ -0,0 +1,101 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { root, pkg, fail } from './runtime.mjs';
7
+
8
+ export const stateRoot = () => process.env.SEEDAUDIO_STATE_DIR || (process.platform === 'win32' ? path.join(process.env.LOCALAPPDATA || os.homedir(), 'seedaudiocli') : path.join(os.homedir(), '.local', 'state', 'seedaudiocli'));
9
+ export function newer(a, b) {
10
+ if (!/^\d+\.\d+\.\d+$/.test(a)) return false;
11
+ const x=a.split('.').map(Number), y=b.split('.').map(Number);
12
+ for(let i=0;i<3;i++) { if(x[i]!==y[i]) return x[i]>y[i]; }
13
+ return false;
14
+ }
15
+ export function globalPrefix(location=root, platform=process.platform) {
16
+ const modules=path.dirname(path.dirname(location));
17
+ if(path.basename(modules)!=='node_modules' || fs.existsSync(path.join(location,'.git'))) return null;
18
+ if(platform==='win32') return path.dirname(modules);
19
+ return path.basename(path.dirname(modules))==='lib' ? path.dirname(path.dirname(modules)) : null;
20
+ }
21
+ export async function checkUpdate(registry=fetch) {
22
+ try {
23
+ const r=await registry(`https://registry.npmjs.org/${encodeURIComponent(pkg.name)}/latest`,{signal:AbortSignal.timeout(8000)});
24
+ if(!r.ok) throw Error('Registry unavailable');
25
+ const data=await r.json();
26
+ if(data.name!==pkg.name || !/^\d+\.\d+\.\d+$/.test(data.version)) throw Error('Invalid stable package');
27
+ return {ok:true,current:pkg.version,available:data.version,updateAvailable:newer(data.version,pkg.version),tag:'latest'};
28
+ } catch { return {ok:false,code:'REGISTRY_UNAVAILABLE',current:pkg.version,updateAvailable:false,offline:'keep-current'}; }
29
+ }
30
+ function npm(args) {
31
+ let cli=process.env.npm_execpath;
32
+ if(!cli?.endsWith('npm-cli.js')) {
33
+ const found=spawnSync(process.platform==='win32'?'where':'which',[process.platform==='win32'?'npm.cmd':'npm'],{encoding:'utf8',timeout:10000});
34
+ const launcher=found.stdout?.trim().split(/\r?\n/)[0];
35
+ cli=launcher && (process.platform==='win32' ? path.join(path.dirname(launcher),'node_modules','npm','bin','npm-cli.js') : fs.realpathSync(launcher));
36
+ }
37
+ if(!cli || !fs.existsSync(cli)) throw fail('NPM_UNAVAILABLE','npm is required for updates.');
38
+ const r=spawnSync(process.execPath,[cli,...args],{encoding:'utf8',timeout:180000});
39
+ if(r.status!==0) throw fail('UPDATE_FAILED','npm update failed; check npm connectivity and installation permissions.');
40
+ return r.stdout;
41
+ }
42
+ export function installUpdate(check, services={}) {
43
+ const prefix=services.prefix || globalPrefix();
44
+ if(!prefix) throw fail('UPDATE_OWNER_REQUIRED','This installation is managed by its source checkout or parent package.');
45
+ const state=stateRoot(); fs.mkdirSync(state,{recursive:true});
46
+ const lock=path.join(state,'update.lock');
47
+ try { fs.mkdirSync(lock); } catch { throw fail('UPDATE_BUSY','Another update is running. Retry this command after it finishes.'); }
48
+ const run=services.npm || npm, spawn=services.spawn || spawnSync;
49
+ const backup=path.join(state,'package-backups',crypto.randomUUID());
50
+ const targets=[];
51
+ try {
52
+ // Recheck the live package under the lock; another process may have updated it.
53
+ const live=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8'));
54
+ if(!newer(check.available,live.version)) return {ok:true,changed:false,version:live.version};
55
+ fs.mkdirSync(backup,{recursive:true});
56
+ const archive=JSON.parse(run(['pack',root,'--ignore-scripts','--pack-destination',backup,'--json']))[0];
57
+ const registered=path.join(state,'skill-targets');
58
+ const known=[path.join(process.env.CODEX_HOME || path.join(os.homedir(),'.codex'),'skills','seedaudiocli')];
59
+ for(const f of (fs.existsSync(registered) ? fs.readdirSync(registered) : [])) {
60
+ try { known.push(JSON.parse(fs.readFileSync(path.join(registered,f),'utf8')).target); } catch { /* invalid registration */ }
61
+ }
62
+ for(const target of new Set(known)) {
63
+ try { if(JSON.parse(fs.readFileSync(path.join(target,'.seedaudiocli-managed.json'),'utf8')).package===pkg.name) targets.push(target); } catch { /* unmanaged or absent */ }
64
+ }
65
+ const entry=path.join(root,'npm','cli.mjs');
66
+ const sync=target=>spawn(process.execPath,[entry,'skill','update','--root',path.dirname(target)],{encoding:'utf8',timeout:30000,env:{...process.env,SEEDAUDIO_AUTO_UPDATE:'0'}});
67
+ let mutated=false;
68
+ try {
69
+ mutated=true;
70
+ run(['install','--global','--prefix',prefix,'--ignore-scripts','--no-audit','--no-fund',`${pkg.name}@${check.available}`]);
71
+ const version=JSON.parse(fs.readFileSync(path.join(root,'package.json'),'utf8')).version;
72
+ if(version!==check.available) throw fail('UPDATE_VERIFY_FAILED','Installed package version mismatch.');
73
+ for(const target of targets) if(sync(target).status!==0) throw fail('SKILL_SYNC_FAILED','A managed Skill needs reconciliation.');
74
+ return {ok:true,changed:true,from:pkg.version,version,backup:path.join(backup,archive.filename)};
75
+ } catch(e) {
76
+ if(mutated) {
77
+ try { run(['install','--global','--prefix',prefix,'--ignore-scripts',path.join(backup,archive.filename)]); }
78
+ catch { throw fail('ROLLBACK_REQUIRED','Automatic recovery failed; restore the package archive before running commands.'); }
79
+ for(const target of targets) if(sync(target).status!==0) throw fail('ROLLBACK_SYNC_REQUIRED','Package restored; reconcile edited Skill files before updating.');
80
+ }
81
+ throw e;
82
+ }
83
+ } finally { fs.rmdirSync(lock); }
84
+ }
85
+ export async function beforeRun(args, services={}) {
86
+ if(process.env.SEEDAUDIO_AUTO_UPDATE==='0' || !globalPrefix() || args[0]==='update') return false;
87
+ const check=await (services.check || checkUpdate)();
88
+ if(!check.ok) console.error('[seedaudiocli] Update check unavailable; using installed version.');
89
+ if(!check.updateAvailable) return false;
90
+ try {
91
+ const result=(services.install || installUpdate)(check);
92
+ console.error(`[seedaudiocli] Using latest ${result.version}.`);
93
+ } catch(e) {
94
+ if(['UPDATE_BUSY','ROLLBACK_SYNC_REQUIRED','ROLLBACK_REQUIRED'].includes(e.code)) throw e;
95
+ console.error(`[seedaudiocli] ${e.code || 'UPDATE_FAILED'}; using restored installed version.`);
96
+ }
97
+ // Delegate once only after update/rollback; never replay a business request.
98
+ const r=spawnSync(process.execPath,[path.join(root,'npm','cli.mjs'),...args],{stdio:'inherit',env:{...process.env,SEEDAUDIO_AUTO_UPDATE:'0'}});
99
+ process.exitCode=r.status??1;
100
+ return true;
101
+ }
package/package.json CHANGED
@@ -1,6 +1,35 @@
1
1
  {
2
2
  "name": "@petercjl/seedaudiocli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.2",
4
+ "description": "Seed Audio CLI with portable Agent skill and persistent user credentials",
5
+ "type": "module",
6
+ "bin": {
7
+ "seedaudiocli": "npm/cli.mjs"
8
+ },
9
+ "files": [
10
+ "npm/",
11
+ "seedaudiocli/*.py",
12
+ "skills/",
13
+ "capabilities.json",
14
+ "adapters/",
15
+ "seedaudiocli.env.example",
16
+ "seedaudiocli.config.example.json",
17
+ "README.md"
18
+ ],
19
+ "engines": {
20
+ "node": ">=22"
21
+ },
22
+ "scripts": {
23
+ "check": "node --check npm/cli.mjs && node --check npm/runtime.mjs && node --check npm/skills.mjs",
24
+ "test": "node --test test/*.test.mjs",
25
+ "prepack": "npm run check && npm test"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/petercjl/seedaudiocli.git"
33
+ },
34
+ "license": "UNLICENSED"
35
+ }
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())