editmamei 0.22.0 → 0.22.1

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.
Binary file
Binary file
package/dist/cli/help.js CHANGED
@@ -9,6 +9,8 @@ Usage:
9
9
  editmamei config Get/set settings (telemetry, privacy) in ~/.editmamei/settings.json
10
10
  editmamei activate <key> Activate a Pro license on this device
11
11
  editmamei deactivate Free this device's seat (before moving Pro to another machine)
12
+ editmamei repair Re-download the Pro module if it wedged after a host update
13
+ (fixes it without deleting ~/.editmamei — keeps templates + license)
12
14
  editmamei license Show the current license + whether Pro is unlocked
13
15
  editmamei report Write an anonymized diagnostic bundle to Downloads for a bug report
14
16
  editmamei help, --help Print this help
@@ -0,0 +1,36 @@
1
+ import { readLicense } from '../license/store.js';
2
+ import { provisionModules } from '../delivery/provision.js';
3
+ export async function runRepair(opts = {}) {
4
+ const out = opts.stdout ?? ((s) => process.stdout.write(s));
5
+ const err = opts.stderr ?? ((s) => process.stderr.write(s));
6
+ const license = readLicense({ dir: opts.dir });
7
+ if (!license) {
8
+ err('No Pro license found on this device. Run `editmamei activate <license-key>` first.\n');
9
+ throw new Error('no license');
10
+ }
11
+ out('Repairing the Pro module (re-provisioning from the delivery service)…\n');
12
+ const prov = await provisionModules(license.key, {
13
+ force: true,
14
+ dir: opts.dir,
15
+ now: opts.now,
16
+ config: opts.delivery?.config,
17
+ fetchImpl: opts.delivery?.fetchImpl,
18
+ signingKeys: opts.delivery?.signingKeys,
19
+ sleep: opts.delivery?.sleep,
20
+ });
21
+ if (prov.notConfigured) {
22
+ out(' Module delivery is not configured in this build — nothing to repair.\n');
23
+ return;
24
+ }
25
+ for (const m of prov.installed)
26
+ out(` Installed ${m.sku} module v${m.version}.\n`);
27
+ for (const s of prov.skipped)
28
+ out(` Skipped ${s.sku} v${s.version}: ${s.reason}.\n`);
29
+ for (const e of prov.errors) {
30
+ err(` Error: could not provision the ${e.sku} module: ${e.message}\n`);
31
+ }
32
+ if (prov.errors.length > 0) {
33
+ throw new Error('module re-provisioning failed');
34
+ }
35
+ out('\nRestart your MCP client (Claude Desktop / Claude Code) to load Pro tools.\n');
36
+ }
@@ -4,6 +4,7 @@ import { runStatus } from './status.js';
4
4
  import { runConfig } from './config.js';
5
5
  import { runActivate } from './activate.js';
6
6
  import { runDeactivate } from './deactivate.js';
7
+ import { runRepair } from './repair.js';
7
8
  import { runLicenseStatus } from './license.js';
8
9
  import { runReport } from './report.js';
9
10
  import { printHelp } from './help.js';
@@ -39,6 +40,9 @@ export async function routeCli(argv, opts = {}) {
39
40
  case 'deactivate':
40
41
  await runDeactivate({ stderr: err });
41
42
  return { handled: true, exitCode: 0 };
43
+ case 'repair':
44
+ await runRepair({ stderr: err });
45
+ return { handled: true, exitCode: 0 };
42
46
  case 'license':
43
47
  await runLicenseStatus();
44
48
  return { handled: true, exitCode: 0 };
@@ -16,10 +16,14 @@ import { join, dirname } from 'node:path';
16
16
  import { pathToFileURL } from 'node:url';
17
17
  import { GoSnippetClient, resolveProBinaryPath, coreBinaryName } from '../api/snippet-client.js';
18
18
  import { isProEntitled } from '../license/entitlement.js';
19
- import { loadVerifiedModule, PRO_SKU } from '../delivery/store.js';
19
+ import { loadVerifiedModule, readInstalledModule, installedPath, PRO_SKU, } from '../delivery/store.js';
20
+ import { provisionModules } from '../delivery/provision.js';
21
+ import { readLicense } from '../license/store.js';
22
+ import { existsSync } from 'node:fs';
20
23
  import { runScript } from '../utils/run-script.js';
21
24
  import { listTemplates } from '../utils/template-storage.js';
22
25
  import { Kernel } from '../kernel/kernel.js';
26
+ import { HOST_MIN_ABI } from '../kernel/host-api.js';
23
27
  import { ceModule } from '../modules/ce/index.js';
24
28
  let logScriptOnErrorWarned = false;
25
29
  function warnLogScriptOnErrorOnce(logger) {
@@ -49,6 +53,7 @@ export class EditmameiServer {
49
53
  telemetry;
50
54
  kernel;
51
55
  proModule = null;
56
+ moduleSkipReason = null;
52
57
  psVersion = null;
53
58
  updateInfo = null;
54
59
  snippetClient = new GoSnippetClient();
@@ -202,16 +207,22 @@ export class EditmameiServer {
202
207
  if (isProEntitled()) {
203
208
  const verified = loadVerifiedModule(PRO_SKU);
204
209
  if (verified) {
210
+ const abi = readInstalledModule(PRO_SKU)?.abi ?? null;
205
211
  return {
206
212
  importer: () => import(pathToFileURL(verified.handlersPath).href),
207
213
  binDir: verified.binDir,
214
+ abi,
208
215
  };
209
216
  }
217
+ if (EDITION !== 'dev' && existsSync(installedPath(PRO_SKU))) {
218
+ this.moduleSkipReason = 'corrupt';
219
+ }
210
220
  }
211
221
  if (EDITION === 'dev') {
212
222
  return {
213
223
  importer: () => import('../modules/pro/index.js'),
214
224
  binDir: dirname(resolveProBinaryPath()),
225
+ abi: null,
215
226
  };
216
227
  }
217
228
  return null;
@@ -219,14 +230,73 @@ export class EditmameiServer {
219
230
  async loadModules() {
220
231
  if (!this.proModule)
221
232
  return;
233
+ if (this.proModule.abi !== null && this.proModule.abi < HOST_MIN_ABI) {
234
+ this.logger.warn(`Pro module (abi ${this.proModule.abi}) is older than this host requires ` +
235
+ `(min abi ${HOST_MIN_ABI}) — booting Community; will re-provision in the background.`);
236
+ this.moduleSkipReason = 'incompatible';
237
+ return;
238
+ }
239
+ const snapshot = this.toolRegistry.snapshot();
222
240
  try {
223
241
  await this.kernel.loadDownloaded(this.proModule.importer);
242
+ this.assertToolsClassified();
224
243
  }
225
244
  catch (err) {
226
- this.logger.warn(`Pro module not loaded: ${err instanceof Error ? err.message : String(err)}`);
245
+ const changed = this.toolRegistry.count() - snapshot.size;
246
+ this.toolRegistry.restore(snapshot);
247
+ this.logger.warn(`Pro module could not be loaded on this host — booting Community and rolling back ` +
248
+ `${changed} module tool change(s); will re-provision in the background: ` +
249
+ `${err instanceof Error ? err.message : String(err)}`);
250
+ this.moduleSkipReason = 'incompatible';
251
+ }
252
+ }
253
+ async reprovisionIfModuleSkipped(delivery = {}) {
254
+ const reason = this.moduleSkipReason;
255
+ if (reason === null)
256
+ return;
257
+ const license = readLicense();
258
+ if (!license) {
259
+ this.logger.warn('A Pro module was skipped but no cached license was found — staying Community. ' +
260
+ 'Run `editmamei activate <key>` to restore Pro.');
227
261
  return;
228
262
  }
229
- this.assertToolsClassified();
263
+ try {
264
+ const prov = await provisionModules(license.key, {
265
+ force: reason === 'corrupt',
266
+ config: delivery.config,
267
+ fetchImpl: delivery.fetchImpl,
268
+ signingKeys: delivery.signingKeys,
269
+ sleep: delivery.sleep,
270
+ });
271
+ if (prov.installed.length > 0) {
272
+ for (const m of prov.installed) {
273
+ this.logger.info(`Pro module updated to v${m.version}, restart to load.`);
274
+ }
275
+ return;
276
+ }
277
+ if (prov.notConfigured) {
278
+ this.logger.warn('Module delivery is not configured — staying Community.');
279
+ return;
280
+ }
281
+ for (const e of prov.errors) {
282
+ this.logger.warn(`Could not re-provision the ${e.sku} module (staying Community): ${e.message}`);
283
+ }
284
+ if (prov.errors.length === 0) {
285
+ if (reason === 'incompatible') {
286
+ this.logger.warn('The published Pro module does not yet support this host version — staying ' +
287
+ 'Community. Update Editmamei when a compatible release ships; ' +
288
+ '`editmamei report` files a diagnostic.');
289
+ }
290
+ else {
291
+ this.logger.warn('Re-provision installed nothing for the corrupt Pro module — staying ' +
292
+ 'Community. Try `editmamei repair`; `editmamei report` if it persists.');
293
+ }
294
+ }
295
+ }
296
+ catch (err) {
297
+ this.logger.warn(`Background Pro-module re-provision failed (staying Community): ` +
298
+ `${err instanceof Error ? err.message : String(err)}`);
299
+ }
230
300
  }
231
301
  assertToolsClassified() {
232
302
  for (const tool of this.toolRegistry.list()) {
@@ -381,6 +451,7 @@ export class EditmameiServer {
381
451
  const transport = new StdioServerTransport();
382
452
  await this.server.connect(transport);
383
453
  void this.session.initialize().catch(() => undefined);
454
+ void this.reprovisionIfModuleSkipped();
384
455
  this.telemetry.start();
385
456
  void this.telemetry.flushOutboxOnStartup();
386
457
  this.server.onclose = () => {
@@ -20,6 +20,12 @@ export class ToolRegistry {
20
20
  this.register(def.tool.name, def);
21
21
  }
22
22
  }
23
+ snapshot() {
24
+ return new Map(this.tools);
25
+ }
26
+ restore(snap) {
27
+ this.tools = new Map(snap);
28
+ }
23
29
  get(name) {
24
30
  return this.tools.get(name);
25
31
  }
@@ -51,7 +51,7 @@ export async function provisionModules(key, opts = {}) {
51
51
  continue;
52
52
  }
53
53
  const installed = readInstalledModule(sku, opts);
54
- if (installed && installed.version === latest) {
54
+ if (installed && installed.version === latest && !opts.force) {
55
55
  result.skipped.push({ sku, version: latest, reason: 'up-to-date' });
56
56
  continue;
57
57
  }
@@ -59,7 +59,7 @@ export async function provisionModules(key, opts = {}) {
59
59
  result.skipped.push({
60
60
  sku,
61
61
  version: latest,
62
- reason: `rollback-blocked (installed v${installed.version} is newer than manifest latest v${latest})`,
62
+ reason: `downgrade-blocked (installed v${installed.version} is newer than manifest latest v${latest})`,
63
63
  });
64
64
  continue;
65
65
  }
@@ -1,5 +1,5 @@
1
1
  import { join, dirname } from 'node:path';
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, rmSync, lstatSync, } from 'node:fs';
3
3
  import { settingsDir } from '../core/settings.js';
4
4
  import { installBundle } from './bundle.js';
5
5
  import { sha256Hex } from './crypto.js';
@@ -7,6 +7,8 @@ import { verifyModuleSignature, MODULE_SIGNING_PUBLIC_KEYS } from './signing.js'
7
7
  import { Logger } from '../utils/logger.js';
8
8
  const logger = new Logger('Modules');
9
9
  const INSTALLED_FILENAME = 'installed.json';
10
+ const TMP_PREFIX = '.tmp-';
11
+ const TMP_STALE_MS = 5 * 60 * 1000;
10
12
  export const PRO_SKU = 'pro';
11
13
  export const PRO_HANDLERS_ENTRY = 'pro-handlers.mjs';
12
14
  const MODULE_MANIFEST = 'manifest.json';
@@ -72,14 +74,71 @@ function writeFileAtomic(path, data) {
72
74
  renameSync(tmp, path);
73
75
  }
74
76
  export function installModule(rec, blob, opts = {}) {
75
- const dir = installedModuleDir(rec.sku, rec.version, opts);
76
- installBundle(blob, rec.content_key, dir);
77
- writeFileSync(moduleArtifactPath(rec.sku, rec.version, opts), Buffer.from(blob), { mode: 0o600 });
77
+ const finalDir = installedModuleDir(rec.sku, rec.version, opts);
78
+ const skuDir = dirname(finalDir);
79
+ const tmpDir = join(skuDir, `${TMP_PREFIX}${rec.version}-${process.pid}`);
80
+ rmSync(tmpDir, { recursive: true, force: true });
81
+ try {
82
+ installBundle(blob, rec.content_key, tmpDir);
83
+ writeFileSync(join(tmpDir, MODULE_ARTIFACT), Buffer.from(blob), { mode: 0o600 });
84
+ }
85
+ catch (err) {
86
+ rmSync(tmpDir, { recursive: true, force: true });
87
+ throw err;
88
+ }
89
+ if (opts.force && existsSync(finalDir)) {
90
+ rmSync(finalDir, { recursive: true, force: true });
91
+ }
92
+ try {
93
+ renameSync(tmpDir, finalDir);
94
+ }
95
+ catch (err) {
96
+ rmSync(tmpDir, { recursive: true, force: true });
97
+ if (!existsSync(finalDir))
98
+ throw err;
99
+ }
78
100
  const now = opts.now ?? Date.now;
79
101
  const installed = { ...rec, installed_at: new Date(now()).toISOString() };
80
102
  writeFileAtomic(installedPath(rec.sku, opts), JSON.stringify(installed, null, 2) + '\n');
103
+ pruneOldModuleVersions(rec.sku, rec.version, opts);
81
104
  return installed;
82
105
  }
106
+ export function pruneOldModuleVersions(sku, keepVersion, opts = {}) {
107
+ const skuDir = join(modulesRoot(opts), sku);
108
+ if (!existsSync(skuDir))
109
+ return;
110
+ let entries;
111
+ try {
112
+ entries = readdirSync(skuDir);
113
+ }
114
+ catch (err) {
115
+ logger.warn(`could not list module dir for '${sku}' to prune: ${err instanceof Error ? err.message : String(err)}`);
116
+ return;
117
+ }
118
+ const now = (opts.now ?? Date.now)();
119
+ for (const entry of entries) {
120
+ if (entry === keepVersion || entry === INSTALLED_FILENAME)
121
+ continue;
122
+ const full = join(skuDir, entry);
123
+ try {
124
+ const st = lstatSync(full);
125
+ if (entry.startsWith(TMP_PREFIX)) {
126
+ if (now - st.mtimeMs > TMP_STALE_MS) {
127
+ rmSync(full, { recursive: true, force: true });
128
+ logger.info(`Pruned stale staging dir '${sku}/${entry}'.`);
129
+ }
130
+ continue;
131
+ }
132
+ if (!st.isDirectory())
133
+ continue;
134
+ rmSync(full, { recursive: true, force: true });
135
+ logger.info(`Pruned stale module version '${sku}' v${entry}.`);
136
+ }
137
+ catch (err) {
138
+ logger.warn(`could not prune stale module entry '${sku}/${entry}': ${err instanceof Error ? err.message : String(err)}`);
139
+ }
140
+ }
141
+ }
83
142
  export function loadVerifiedModule(sku, opts = {}, pubKeys = MODULE_SIGNING_PUBLIC_KEYS) {
84
143
  const rec = readInstalledModule(sku, opts);
85
144
  if (!rec)
@@ -1 +1,2 @@
1
1
  export const KERNEL_ABI = 1;
2
+ export const HOST_MIN_ABI = 1;
Binary file
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.22.0';
1
+ export const VERSION = '0.22.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "editmamei",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "description": "Editmamei — Unlock Photoshop with natural-language photo editing (Community Edition)",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",