@petercjl/seedaudiocli 0.1.1 → 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
@@ -57,10 +57,14 @@ npm install -g @petercjl/seedaudiocli@latest
57
57
  seedaudiocli skill update --agent codex
58
58
  ```
59
59
 
60
- Run `skill update` for each installed Agent, including the configured SealSeek root. `update instructions` prints these operations and the current version's rollback command. Updates are explicit. Review compatibility before a major-version update. For rollback, install the prior exact npm version and synchronize Skills again. Credentials and history remain unchanged; offline or registry errors require retrying the package update when connectivity returns.
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
61
 
62
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
63
 
64
64
  ## Development and evidence
65
65
 
66
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.
package/capabilities.json CHANGED
@@ -27,7 +27,8 @@
27
27
  "skill status",
28
28
  "skill install",
29
29
  "skill update",
30
- "update instructions"
30
+ "update check",
31
+ "update install"
31
32
  ],
32
33
  "config": {
33
34
  "format": [
@@ -52,10 +53,15 @@
52
53
  "secrets_in_output": false
53
54
  },
54
55
  "update": {
55
- "trigger": "explicit",
56
- "instructions": "npm install -g @petercjl/seedaudiocli@latest; then skill update for each installed Agent",
57
- "compatibility": "Review release notes before major updates",
58
- "configuration": "Outside package; preserved"
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"
59
65
  },
60
66
  "generation": {
61
67
  "provider": "EvoLink",
package/npm/cli.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import fs from 'node:fs';
3
+ import { beforeRun, checkUpdate, installUpdate } from './update.mjs';
3
4
  import path from 'node:path';
4
5
  import { root, pkg, fail, findPython, runPython } from './runtime.mjs';
5
6
  import { source, targetPath, status, install } from './skills.mjs';
@@ -8,7 +9,8 @@ const args = process.argv.slice(2);
8
9
  const print = value => console.log(JSON.stringify(value, null, 2));
9
10
  const flag = name => { const i = args.indexOf(name); return i < 0 ? undefined : args[i + 1]; };
10
11
  try {
11
- if (['version', '--version'].includes(args[0])) console.log(`seedaudiocli ${pkg.version}`);
12
+ if (await beforeRun(args)) { /* child completed */ }
13
+ else if (['version', '--version'].includes(args[0])) console.log(`seedaudiocli ${pkg.version}`);
12
14
  else if (args[0] === 'capabilities') print(JSON.parse(fs.readFileSync(path.join(root, 'capabilities.json'), 'utf8')));
13
15
  else if (args[0] === 'doctor') {
14
16
  const python = findPython();
@@ -26,9 +28,11 @@ try {
26
28
  else throw fail('INVALID_COMMAND', 'Use skill source|status|install|update.');
27
29
  }
28
30
  } else if (args[0] === 'update') {
29
- print({ ok: true, policy: 'explicit', installed_version: pkg.version, package_command: `npm install -g ${pkg.name}@latest`, skill_command: 'seedaudiocli skill update --agent AGENT [--root SKILLS_DIRECTORY]', rollback: `npm install -g ${pkg.name}@${pkg.version}`, note: 'Run the package command, then update each previously installed Skill. Configuration and task history remain in user directories. Registry/network failures preserve the last saved configuration.' });
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' });
30
34
  } else if (!args.length || ['--help', '-h'].includes(args[0])) {
31
- 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 instructions\nRuntime: Node.js 22+, Python 3.9+ (SEEDAUDIO_PYTHON optional).');
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).');
32
36
  process.exitCode = runPython(['--help']).status || 0;
33
37
  } else {
34
38
  const result = runPython(args);
package/npm/skills.mjs CHANGED
@@ -2,6 +2,7 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import crypto from 'node:crypto';
5
+ import { stateRoot } from './update.mjs';
5
6
  import { root, pkg, fail } from './runtime.mjs';
6
7
 
7
8
  export const source = path.join(root, 'skills', 'seedaudiocli');
@@ -46,6 +47,10 @@ export function status(target) {
46
47
  }
47
48
  export function install(target) {
48
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 });
49
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.');
50
55
  if (state.edited) throw fail('SKILL_EDITED', 'Managed Skill files contain local edits; preserve and reconcile those edits before updating.');
51
56
  if (state.current && state.version === pkg.version) return { ...state, record: undefined, ok: true, changed: false };
@@ -60,7 +65,8 @@ export function install(target) {
60
65
  let backup;
61
66
  try {
62
67
  if (state.installed) {
63
- backup = target + '.backup-' + crypto.randomUUID();
68
+ backup = path.join(stateRoot(), 'skill-backups', crypto.randomUUID());
69
+ fs.mkdirSync(path.dirname(backup), { recursive: true });
64
70
  fs.cpSync(target, backup, { recursive: true, errorOnExist: true, force: false });
65
71
  } else fs.mkdirSync(target);
66
72
  // In-place updates support hosts holding a directory handle open. Backup is recoverable.
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,6 @@
1
1
  {
2
2
  "name": "@petercjl/seedaudiocli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Seed Audio CLI with portable Agent skill and persistent user credentials",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,4 +52,4 @@ Use `@audio1` through `@audio3` in prompts to address the corresponding audio re
52
52
 
53
53
  `seedaudiocli skill install --agent codex` installs a managed copy. For SealSeek supply the actual discovery directory with `--root SKILLS_DIRECTORY` or `SEALSEEK_SKILLS_DIR`; installation refuses to guess the host's layout. Existing unmanaged Skills are preserved. `skill status` reports ownership and freshness; `skill update` backs up managed files and preserves customized UI metadata. Edited managed content requires reconciliation.
54
54
 
55
- Run `seedaudiocli update instructions` for the package update and Skill synchronization commands. Updates are explicit; configuration and history live outside the npm package and remain available afterward. Record recurring contract failures for a separately authorized package update.
55
+ Global npm installations check stable `latest` before every invocation and install a newer version before executing the command once. Managed Skills are synchronized with backups. Offline checks retain the installed version. `seedaudiocli update check` inspects availability; `update install` performs an explicit update. Set `SEEDAUDIO_AUTO_UPDATE=0` for a deliberately pinned run. Development checkouts remain Git-managed. Within taobao-ai-ops use `taobao-ai-ops component run seedaudiocli ...`; the suite owns component updates. Configuration and task history remain in user directories. Record recurring contract failures for a separately authorized package update.