rewritable 0.8.1 → 0.9.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/bin/rwa.mjs CHANGED
@@ -71,6 +71,12 @@ Usage:
71
71
  editing — the returned url carries the capability
72
72
  token in its #k= fragment. Target: --url >
73
73
  \$RWA_HOST_URL. --json emits {id,token,url}.
74
+ rwa install <skill> <host> install a signed .rwa-skill.json into a skill-host
75
+ container, offline. Verifies the signature + runs the
76
+ same trust gates as the in-app dialog, then splices the
77
+ skill into the frozen #rwa-skills zone. Requires --yes
78
+ (no dialog to consent in); gate failures are final.
79
+ --json emits {skillId,name,kind,verified,status}.
74
80
  rwa skin <path> <name> apply a named style preset to a rewritable in
75
81
  place (deterministic, offline, model-free). Names:
76
82
  notion-clean, linear-dark, editorial-serif,
@@ -826,6 +832,66 @@ function detectProductKind(fileText) {
826
832
  return;
827
833
  }
828
834
 
835
+ // `rwa install <skill.rwa-skill.json> <skill-host.html> [--yes|--trust] [--json]` (v0.9 §3 / I11).
836
+ // The offline, headless counterpart of the seed's install dialog: verify the Ed25519
837
+ // signature, run the SAME gates (unsigned-capability / compute-with-perms / permission
838
+ // grammar / dynamic-import reject), then splice the envelope into the frozen #rwa-skills
839
+ // zone and write atomically. No dialog to consent in → an explicit --yes/--trust is
840
+ // required, and gate failures (exit 3) are FINAL — --yes cannot override them. Exit codes:
841
+ // 1 usage, 2 file, 3 envelope/gate (reuses codeName — no verb-specific exit-4 class). See
842
+ // src/install.mjs. The CLI is the sole audited exception to runtime-sole-writer (Inv 39).
843
+ if (verb === 'install') {
844
+ const jsonMode = rest.includes('--json');
845
+ const consent = rest.includes('--yes') || rest.includes('--trust');
846
+ const [envPath, hostPath] = rest.filter((a) => !a.startsWith('-'));
847
+ const emitInstall = (payload) => {
848
+ if (jsonMode) { process.stderr.write(JSON.stringify(payload) + '\n'); return; }
849
+ const parts = [payload.code, payload.subcode].filter(Boolean);
850
+ let line = 'rwa install: ' + parts.join('/');
851
+ if (payload.details && Object.keys(payload.details).length) line += ' ' + JSON.stringify(payload.details);
852
+ process.stderr.write(line + '\n');
853
+ };
854
+ if (!envPath || !hostPath) {
855
+ emitInstall({ code: 'usage_error', subcode: 'missing_file_args', details: { usage: 'rwa install <skill.rwa-skill.json> <skill-host.html> [--yes] [--json]' } });
856
+ process.exitCode = 1;
857
+ return;
858
+ }
859
+ const { installSkillFile } = await import('../src/install.mjs');
860
+ let result;
861
+ try {
862
+ result = await installSkillFile(envPath, hostPath, { consent });
863
+ } catch (e) {
864
+ if (e && typeof e.exitCode === 'number') {
865
+ emitInstall({ code: codeName(e.exitCode), subcode: e.subcode, details: e.details });
866
+ process.exitCode = e.exitCode;
867
+ return;
868
+ }
869
+ throw e;
870
+ }
871
+ // Non-blocking lookalike warning (spec §3 / Inv 23) → always to stderr; --json also
872
+ // carries result.lookalike in the stdout object. The install already succeeded.
873
+ if (result.lookalike) {
874
+ process.stderr.write('⚠ rwa install: the name "' + result.name + '" closely matches "' + result.lookalike + '", installed from a DIFFERENT key. The author is identified by the key, not the name — review before trusting.\n');
875
+ }
876
+ // I5 — same-key rename heads-up (non-blocking, registry-derived). This author published other
877
+ // names in this host; surfaced so a rename reads as continuity, not a new author.
878
+ if (Array.isArray(result.priorNames) && result.priorNames.length) {
879
+ process.stderr.write('note: rwa install: this author (same key) previously published: ' + result.priorNames.join(', ') + '. The current name is "' + result.name + '" — identity is the key, not the name.\n');
880
+ }
881
+ if (jsonMode) {
882
+ process.stdout.write(JSON.stringify(result) + '\n');
883
+ } else {
884
+ const label = result.status === 'updated' ? 'Updated' : result.status === 'already_installed' ? 'Already installed' : 'Installed';
885
+ process.stdout.write(
886
+ '✓ ' + label + ' ' + result.name + ' (' + result.kind + ', ' + (result.verified ? 'verified' : 'UNVERIFIED') + ')\n' +
887
+ ' skillId: ' + result.skillId + '\n' +
888
+ (result.update && result.update.added.length ? ' + added: ' + result.update.added.join(', ') + '\n' : '') +
889
+ (result.update && result.update.removed.length ? ' − removed: ' + result.update.removed.join(', ') + '\n' : ''),
890
+ );
891
+ }
892
+ return;
893
+ }
894
+
829
895
  // `rwa skin <file> <name|reset> [--l1] [--theme-only] [--json]`.
830
896
  //
831
897
  // DEFAULT (no --l1): deterministic, model-free theme swap. Applies a preset's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rewritable",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "CLI for re-writeable: emit and import single-file rwa documents.",
5
5
  "type": "module",
6
6
  "bin": {