@awak-app/simy-cli 0.5.7 → 0.5.10
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 +54 -1
- package/package.json +7 -3
- package/src/agent.js +71 -10
- package/src/auto-update.js +312 -35
- package/src/browser.js +24 -7
- package/src/cli-contract.js +1 -1
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/SKILL.md +188 -12
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/acceptance-evidence.md +130 -0
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/change-boundary-matrix.md +20 -1
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/cross-repository-control.md +76 -0
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/delivery-levels.md +76 -0
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/local-sqm-evidence.md +34 -0
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/references/state-and-receipts.md +15 -0
- package/src/codex-plugins/simy/skills/agentic-delivery-loop/scripts/check-acceptance.mjs +191 -0
- package/src/console/app.js +21 -2
- package/src/console/commands.js +3 -0
- package/src/desktop-host.js +220 -0
- package/src/desktop-process-lock.js +231 -0
- package/src/desktop-status.js +263 -0
- package/src/direct-executor.js +6 -0
- package/src/index.js +38 -12
- package/src/install-update.js +494 -0
- package/src/sqm/agent-skill.js +61 -75
- package/src/sqm/command.js +3 -0
- package/src/sqm/local-evidence.js +28 -0
- package/src/sqm/workflow-onboarding.js +1 -1
- package/src/sqm/workflow-update.js +3 -2
package/src/auto-update.js
CHANGED
|
@@ -15,6 +15,12 @@ import path from "node:path";
|
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { promisify } from "node:util";
|
|
17
17
|
|
|
18
|
+
import { openExternalUrl } from "./browser.js";
|
|
19
|
+
import {
|
|
20
|
+
installMacosInstallerRelease,
|
|
21
|
+
readCurrentInstallerVersion,
|
|
22
|
+
} from "./install-update.js";
|
|
23
|
+
|
|
18
24
|
const execFileAsync = promisify(execFile);
|
|
19
25
|
|
|
20
26
|
export const CLI_PACKAGE_NAME = "@awak-app/simy-cli";
|
|
@@ -26,6 +32,8 @@ const AUTO_UPDATE_IDLE_RETRY_MS = 30_000;
|
|
|
26
32
|
const UPDATE_LOCK_STALE_MS = 30 * 60 * 1_000;
|
|
27
33
|
const RESTART_HANDOFF_TIMEOUT_MS = 30_000;
|
|
28
34
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
35
|
+
const INSTALLATION_MARKER_FILE = "installation.json";
|
|
36
|
+
const MACOS_INSTALLER_MODE = "macos_installer";
|
|
29
37
|
const NON_IDLE_STATES = new Set([
|
|
30
38
|
"queued",
|
|
31
39
|
"risk_classifying",
|
|
@@ -73,7 +81,8 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
73
81
|
automatic: Boolean(
|
|
74
82
|
!options.disabled &&
|
|
75
83
|
options.daemon &&
|
|
76
|
-
options.installMode === "global_npm"
|
|
84
|
+
(options.installMode === "global_npm" ||
|
|
85
|
+
(options.installMode === MACOS_INSTALLER_MODE && options.installerMarker?.teamId)) &&
|
|
77
86
|
options.requestedPort === 0
|
|
78
87
|
),
|
|
79
88
|
checked_at: null,
|
|
@@ -89,6 +98,17 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
89
98
|
return structuredClone(this.#snapshot);
|
|
90
99
|
}
|
|
91
100
|
|
|
101
|
+
async performAction() {
|
|
102
|
+
const action = this.#snapshot.action;
|
|
103
|
+
if (action?.id === "download_macos_installer" && action.url) {
|
|
104
|
+
return this.#options.openUrl(action.url);
|
|
105
|
+
}
|
|
106
|
+
if (action?.command) {
|
|
107
|
+
throw new Error(`Run this command after leaving SIMY: ${action.command}`);
|
|
108
|
+
}
|
|
109
|
+
throw new Error("No SIMY CLI update action is currently available.");
|
|
110
|
+
}
|
|
111
|
+
|
|
92
112
|
acceptsNewWork() {
|
|
93
113
|
return !["updating", "restarting"].includes(this.#snapshot.state);
|
|
94
114
|
}
|
|
@@ -151,9 +171,10 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
151
171
|
});
|
|
152
172
|
try {
|
|
153
173
|
const release = await this.#options.checkLatestVersion();
|
|
174
|
+
this.#options.lastRelease = release;
|
|
154
175
|
const targetVersion = String(release?.version || "").trim();
|
|
155
176
|
if (!parseStableVersion(targetVersion)) {
|
|
156
|
-
throw new Error("The
|
|
177
|
+
throw new Error("The update service returned an invalid stable CLI version.");
|
|
157
178
|
}
|
|
158
179
|
const checkedAt = new Date(this.#options.now()).toISOString();
|
|
159
180
|
if (compareStableVersions(targetVersion, this.#options.currentVersion) <= 0) {
|
|
@@ -169,8 +190,11 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
169
190
|
}
|
|
170
191
|
|
|
171
192
|
const change = classifyVersionChange(this.#options.currentVersion, targetVersion);
|
|
172
|
-
const automatic =
|
|
173
|
-
|
|
193
|
+
const automatic =
|
|
194
|
+
this.#snapshot.automatic &&
|
|
195
|
+
["patch", "minor"].includes(change) &&
|
|
196
|
+
(this.#options.installMode !== MACOS_INSTALLER_MODE || Boolean(release?.artifact?.updater));
|
|
197
|
+
const action = updateAction(targetVersion, this.#options);
|
|
174
198
|
if (!automatic) {
|
|
175
199
|
this.#clearIdleTimer();
|
|
176
200
|
this.#setState("update_available", {
|
|
@@ -241,6 +265,7 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
241
265
|
}
|
|
242
266
|
|
|
243
267
|
let releaseLock = null;
|
|
268
|
+
let installation = null;
|
|
244
269
|
try {
|
|
245
270
|
releaseLock = await this.#options.acquireUpdateLock();
|
|
246
271
|
if (!releaseLock) {
|
|
@@ -256,9 +281,10 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
256
281
|
return this.snapshot();
|
|
257
282
|
}
|
|
258
283
|
|
|
259
|
-
await installAndVerifyVersion({
|
|
284
|
+
installation = await installAndVerifyVersion({
|
|
260
285
|
currentVersion: this.#options.currentVersion,
|
|
261
286
|
targetVersion,
|
|
287
|
+
release: this.#options.lastRelease,
|
|
262
288
|
installVersion: this.#options.installVersion,
|
|
263
289
|
readInstalledVersion: this.#options.readInstalledVersion,
|
|
264
290
|
});
|
|
@@ -269,7 +295,7 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
269
295
|
message: `SIMY CLI ${targetVersion} is installed. Verifying the restarted daemon...`,
|
|
270
296
|
});
|
|
271
297
|
this.#announce();
|
|
272
|
-
await this.#restartDaemon?.(targetVersion);
|
|
298
|
+
await this.#restartDaemon?.(targetVersion, installation);
|
|
273
299
|
this.#clearIdleTimer();
|
|
274
300
|
this.#setState("updated", {
|
|
275
301
|
message: `SIMY CLI ${targetVersion} is running.`,
|
|
@@ -278,10 +304,19 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
278
304
|
});
|
|
279
305
|
return this.snapshot();
|
|
280
306
|
} catch (error) {
|
|
307
|
+
if (installation?.rollback) {
|
|
308
|
+
try {
|
|
309
|
+
await installation.rollback();
|
|
310
|
+
} catch (rollbackError) {
|
|
311
|
+
error = new Error(
|
|
312
|
+
`${errorMessage(error)} Automatic rollback failed (${errorMessage(rollbackError)}).`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
281
316
|
this.#clearIdleTimer();
|
|
282
317
|
this.#setState("failed", {
|
|
283
318
|
message: `Automatic update failed. SIMY ${this.#options.currentVersion} is still running.`,
|
|
284
|
-
action: updateAction(targetVersion),
|
|
319
|
+
action: updateAction(targetVersion, this.#options),
|
|
285
320
|
error: errorMessage(error),
|
|
286
321
|
});
|
|
287
322
|
this.#announce();
|
|
@@ -315,9 +350,11 @@ export class CliAutoUpdater extends EventEmitter {
|
|
|
315
350
|
if (this.#options.quiet) return;
|
|
316
351
|
const suffix =
|
|
317
352
|
["update_available", "failed"].includes(this.#snapshot.state) &&
|
|
318
|
-
this.#snapshot.action?.command
|
|
319
|
-
|
|
320
|
-
|
|
353
|
+
(this.#snapshot.action?.command || this.#snapshot.action?.url)
|
|
354
|
+
? this.#snapshot.action.url
|
|
355
|
+
? ` Open: ${this.#snapshot.action.url}`
|
|
356
|
+
: ` Run manually: ${this.#snapshot.action.command}`
|
|
357
|
+
: "";
|
|
321
358
|
const detail = this.#snapshot.error ? ` (${this.#snapshot.error})` : "";
|
|
322
359
|
this.#options.logger(`${this.#snapshot.message}${suffix}${detail}`);
|
|
323
360
|
}
|
|
@@ -329,6 +366,7 @@ export async function createCliAutoUpdater({
|
|
|
329
366
|
requestedPort = 0,
|
|
330
367
|
disabled = false,
|
|
331
368
|
packageRoot = PACKAGE_ROOT,
|
|
369
|
+
webOrigin,
|
|
332
370
|
dependencies = {},
|
|
333
371
|
} = {}) {
|
|
334
372
|
const packageInfo = dependencies.packageInfo || (await readCliPackageInfo(packageRoot));
|
|
@@ -337,6 +375,14 @@ export async function createCliAutoUpdater({
|
|
|
337
375
|
execFileCommand: dependencies.execFileCommand,
|
|
338
376
|
}));
|
|
339
377
|
const stateRoot = dependencies.stateRoot || defaultStateRoot();
|
|
378
|
+
const installerMarker = installMode === MACOS_INSTALLER_MODE
|
|
379
|
+
? dependencies.installerMarker || (await readInstallerMarker(packageRoot))
|
|
380
|
+
: null;
|
|
381
|
+
const checkRelease = dependencies.checkLatestVersion || (
|
|
382
|
+
installMode === MACOS_INSTALLER_MODE
|
|
383
|
+
? () => checkInstallerRelease({ webOrigin, fetchImpl: dependencies.fetchImpl })
|
|
384
|
+
: checkLatestVersion
|
|
385
|
+
);
|
|
340
386
|
return new CliAutoUpdater({
|
|
341
387
|
daemon: Boolean(daemon),
|
|
342
388
|
interactive: Boolean(interactive),
|
|
@@ -344,15 +390,37 @@ export async function createCliAutoUpdater({
|
|
|
344
390
|
disabled,
|
|
345
391
|
currentVersion: packageInfo.version,
|
|
346
392
|
installMode,
|
|
347
|
-
|
|
393
|
+
installerMarker,
|
|
394
|
+
webOrigin,
|
|
395
|
+
checkLatestVersion: checkRelease,
|
|
348
396
|
installVersion:
|
|
349
397
|
dependencies.installVersion ||
|
|
350
|
-
(
|
|
398
|
+
(installMode === MACOS_INSTALLER_MODE
|
|
399
|
+
? ((version, release) => installMacosInstallerRelease({
|
|
400
|
+
version,
|
|
401
|
+
release,
|
|
402
|
+
marker: installerMarker,
|
|
403
|
+
stateRoot,
|
|
404
|
+
fetchImpl: dependencies.fetchImpl,
|
|
405
|
+
execFileCommand: dependencies.execFileCommand,
|
|
406
|
+
}))
|
|
407
|
+
: ((version) => installGlobalNpmVersion(version, { execFileCommand: dependencies.execFileCommand }))),
|
|
351
408
|
readInstalledVersion:
|
|
352
409
|
dependencies.readInstalledVersion ||
|
|
353
|
-
(
|
|
410
|
+
(installMode === MACOS_INSTALLER_MODE
|
|
411
|
+
? (() => readCurrentInstallerVersion({ stateRoot }))
|
|
412
|
+
: (() => readGlobalNpmVersion({ execFileCommand: dependencies.execFileCommand }))),
|
|
354
413
|
acquireUpdateLock:
|
|
355
|
-
dependencies.acquireUpdateLock || (() => acquireGlobalUpdateLock(stateRoot,
|
|
414
|
+
dependencies.acquireUpdateLock || (() => acquireGlobalUpdateLock(stateRoot, {
|
|
415
|
+
...dependencies,
|
|
416
|
+
installMode,
|
|
417
|
+
})),
|
|
418
|
+
openUrl:
|
|
419
|
+
dependencies.openUrl ||
|
|
420
|
+
((url) => openExternalUrl(url, {
|
|
421
|
+
description: "SIMY installer download page",
|
|
422
|
+
logger: { log() {}, error() {} },
|
|
423
|
+
})),
|
|
356
424
|
logger: dependencies.logger || console.log,
|
|
357
425
|
quiet: Boolean(interactive),
|
|
358
426
|
now: dependencies.now || Date.now,
|
|
@@ -372,8 +440,13 @@ export async function readCliPackageInfo(packageRoot = PACKAGE_ROOT) {
|
|
|
372
440
|
export async function detectInstallMode({
|
|
373
441
|
packageRoot = PACKAGE_ROOT,
|
|
374
442
|
execFileCommand = execFileAsync,
|
|
443
|
+
platform = process.platform,
|
|
444
|
+
arch = process.arch,
|
|
375
445
|
} = {}) {
|
|
376
446
|
const resolvedRoot = await realpathOrResolve(packageRoot);
|
|
447
|
+
if (await readInstallerMarker(resolvedRoot, { platform, arch })) {
|
|
448
|
+
return MACOS_INSTALLER_MODE;
|
|
449
|
+
}
|
|
377
450
|
if (resolvedRoot.split(path.sep).includes("_npx")) return "npx";
|
|
378
451
|
|
|
379
452
|
try {
|
|
@@ -390,6 +463,38 @@ export async function detectInstallMode({
|
|
|
390
463
|
return resolvedRoot.split(path.sep).includes("node_modules") ? "package" : "source";
|
|
391
464
|
}
|
|
392
465
|
|
|
466
|
+
export async function readInstallerMarker(
|
|
467
|
+
packageRoot = PACKAGE_ROOT,
|
|
468
|
+
{ platform = process.platform, arch = process.arch } = {},
|
|
469
|
+
) {
|
|
470
|
+
if (platform !== "darwin" || arch !== "arm64") return null;
|
|
471
|
+
let payload;
|
|
472
|
+
try {
|
|
473
|
+
payload = JSON.parse(
|
|
474
|
+
await readFile(path.join(packageRoot, INSTALLATION_MARKER_FILE), "utf8"),
|
|
475
|
+
);
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (error?.code === "ENOENT" || error instanceof SyntaxError) return null;
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
if (
|
|
481
|
+
!payload ||
|
|
482
|
+
typeof payload !== "object" ||
|
|
483
|
+
Array.isArray(payload) ||
|
|
484
|
+
payload.schemaVersion !== 1 ||
|
|
485
|
+
payload.channel !== "macos-installer" ||
|
|
486
|
+
payload.platform !== "darwin" ||
|
|
487
|
+
payload.arch !== "arm64" ||
|
|
488
|
+
!(payload.teamId === null || /^[A-Z0-9]{10}$/.test(payload.teamId)) ||
|
|
489
|
+
payload.bundleId !== "com.awak.simy.cli" ||
|
|
490
|
+
Object.keys(payload).sort().join(",") !==
|
|
491
|
+
"arch,bundleId,channel,platform,schemaVersion,teamId"
|
|
492
|
+
) {
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
return payload;
|
|
496
|
+
}
|
|
497
|
+
|
|
393
498
|
export async function checkLatestVersion({ fetchImpl = fetch } = {}) {
|
|
394
499
|
const response = await fetchImpl(
|
|
395
500
|
"https://registry.npmjs.org/@awak-app%2fsimy-cli/latest",
|
|
@@ -407,6 +512,74 @@ export async function checkLatestVersion({ fetchImpl = fetch } = {}) {
|
|
|
407
512
|
};
|
|
408
513
|
}
|
|
409
514
|
|
|
515
|
+
export async function checkInstallerRelease({ webOrigin, fetchImpl = fetch } = {}) {
|
|
516
|
+
const origin = new URL(webOrigin).origin;
|
|
517
|
+
const response = await fetchImpl(new URL("/api/local-cli/release", origin), {
|
|
518
|
+
headers: { Accept: "application/json" },
|
|
519
|
+
signal: AbortSignal.timeout(10_000),
|
|
520
|
+
});
|
|
521
|
+
if (!response.ok) {
|
|
522
|
+
throw new Error(`SIMY Web installer release endpoint returned HTTP ${response.status}`);
|
|
523
|
+
}
|
|
524
|
+
const payload = await response.json();
|
|
525
|
+
return validateInstallerRelease(payload);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function validateInstallerRelease(payload) {
|
|
529
|
+
if (
|
|
530
|
+
!payload ||
|
|
531
|
+
typeof payload !== "object" ||
|
|
532
|
+
Array.isArray(payload) ||
|
|
533
|
+
payload.schemaVersion !== 1 ||
|
|
534
|
+
!parseStableVersion(payload.version) ||
|
|
535
|
+
typeof payload.publishedAt !== "string" ||
|
|
536
|
+
!Number.isFinite(Date.parse(payload.publishedAt)) ||
|
|
537
|
+
!Array.isArray(payload.artifacts)
|
|
538
|
+
) {
|
|
539
|
+
throw new Error("SIMY Web returned an invalid installer release manifest.");
|
|
540
|
+
}
|
|
541
|
+
const artifacts = payload.artifacts.filter(
|
|
542
|
+
(artifact) => artifact?.platform === "darwin" && artifact?.arch === "arm64",
|
|
543
|
+
);
|
|
544
|
+
if (artifacts.length !== 1) {
|
|
545
|
+
throw new Error("SIMY Web returned an invalid Apple Silicon installer artifact.");
|
|
546
|
+
}
|
|
547
|
+
const artifact = artifacts[0];
|
|
548
|
+
let artifactUrl;
|
|
549
|
+
try {
|
|
550
|
+
artifactUrl = new URL(artifact.url);
|
|
551
|
+
} catch {
|
|
552
|
+
throw new Error("SIMY Web returned an invalid Apple Silicon installer artifact.");
|
|
553
|
+
}
|
|
554
|
+
if (
|
|
555
|
+
artifactUrl.protocol !== "https:" ||
|
|
556
|
+
!/^[a-f0-9]{64}$/.test(String(artifact.sha256 || "")) ||
|
|
557
|
+
!Number.isSafeInteger(artifact.size) ||
|
|
558
|
+
artifact.size <= 0 ||
|
|
559
|
+
artifact.signed !== true ||
|
|
560
|
+
artifact.notarized !== true
|
|
561
|
+
) {
|
|
562
|
+
throw new Error("SIMY Web returned an invalid Apple Silicon installer artifact.");
|
|
563
|
+
}
|
|
564
|
+
if (artifact.updater != null) {
|
|
565
|
+
let updaterUrl;
|
|
566
|
+
try {
|
|
567
|
+
updaterUrl = new URL(artifact.updater.url);
|
|
568
|
+
} catch {
|
|
569
|
+
throw new Error("SIMY Web returned an invalid automatic updater artifact.");
|
|
570
|
+
}
|
|
571
|
+
if (
|
|
572
|
+
updaterUrl.protocol !== "https:" ||
|
|
573
|
+
!/^[a-f0-9]{64}$/.test(String(artifact.updater.sha256 || "")) ||
|
|
574
|
+
!Number.isSafeInteger(artifact.updater.size) ||
|
|
575
|
+
artifact.updater.size <= 0
|
|
576
|
+
) {
|
|
577
|
+
throw new Error("SIMY Web returned an invalid automatic updater artifact.");
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return { version: payload.version, publishedAt: payload.publishedAt, artifact };
|
|
581
|
+
}
|
|
582
|
+
|
|
410
583
|
export async function installGlobalNpmVersion(
|
|
411
584
|
version,
|
|
412
585
|
{ execFileCommand = execFileAsync } = {},
|
|
@@ -437,20 +610,25 @@ export async function readGlobalNpmVersion({ execFileCommand = execFileAsync } =
|
|
|
437
610
|
export async function installAndVerifyVersion({
|
|
438
611
|
currentVersion,
|
|
439
612
|
targetVersion,
|
|
613
|
+
release,
|
|
440
614
|
installVersion = installGlobalNpmVersion,
|
|
441
615
|
readInstalledVersion = readGlobalNpmVersion,
|
|
442
616
|
}) {
|
|
617
|
+
let installation = null;
|
|
443
618
|
try {
|
|
444
|
-
await installVersion(targetVersion);
|
|
619
|
+
installation = await installVersion(targetVersion, release);
|
|
445
620
|
const installedVersion = await readInstalledVersion();
|
|
446
621
|
if (installedVersion !== targetVersion) {
|
|
447
|
-
throw new Error(`
|
|
622
|
+
throw new Error(`The updater selected SIMY ${installedVersion}; expected ${targetVersion}.`);
|
|
448
623
|
}
|
|
449
|
-
return
|
|
624
|
+
return installation && typeof installation === "object"
|
|
625
|
+
? { ...installation, installedVersion }
|
|
626
|
+
: installedVersion;
|
|
450
627
|
} catch (error) {
|
|
451
628
|
let recoveryError = null;
|
|
452
629
|
try {
|
|
453
|
-
await
|
|
630
|
+
if (installation?.rollback) await installation.rollback();
|
|
631
|
+
else await installVersion(currentVersion);
|
|
454
632
|
const restoredVersion = await readInstalledVersion();
|
|
455
633
|
if (restoredVersion !== currentVersion) {
|
|
456
634
|
throw new Error(`restored version is ${restoredVersion || "unknown"}`);
|
|
@@ -475,6 +653,7 @@ export async function runCliUpdateGate({
|
|
|
475
653
|
incompatibleRetry = false,
|
|
476
654
|
packageRoot = PACKAGE_ROOT,
|
|
477
655
|
entryPath,
|
|
656
|
+
webOrigin,
|
|
478
657
|
env = process.env,
|
|
479
658
|
logger = console.log,
|
|
480
659
|
errorLogger = console.error,
|
|
@@ -513,10 +692,18 @@ export async function runCliUpdateGate({
|
|
|
513
692
|
}
|
|
514
693
|
|
|
515
694
|
let release;
|
|
695
|
+
let releaseInfo;
|
|
516
696
|
try {
|
|
517
|
-
const
|
|
697
|
+
const checkRelease = dependencies.checkLatestVersion || (
|
|
698
|
+
installMode === MACOS_INSTALLER_MODE
|
|
699
|
+
? () => checkInstallerRelease({ webOrigin, fetchImpl: dependencies.fetchImpl })
|
|
700
|
+
: checkLatestVersion
|
|
701
|
+
);
|
|
702
|
+
releaseInfo = await checkRelease();
|
|
518
703
|
release = String(releaseInfo?.version || "").trim();
|
|
519
|
-
if (!parseStableVersion(release))
|
|
704
|
+
if (!parseStableVersion(release)) {
|
|
705
|
+
throw new Error("The update service returned an invalid stable CLI version.");
|
|
706
|
+
}
|
|
520
707
|
} catch (error) {
|
|
521
708
|
if (mode !== "startup") errorLogger(`Update check failed: ${errorMessage(error)}`);
|
|
522
709
|
result.error = errorMessage(error);
|
|
@@ -525,8 +712,31 @@ export async function runCliUpdateGate({
|
|
|
525
712
|
|
|
526
713
|
if (compareStableVersions(release, info.version) <= 0) return report("up_to_date", release);
|
|
527
714
|
const change = classifyVersionChange(info.version, release);
|
|
528
|
-
const action = updateAction(release);
|
|
529
|
-
result.
|
|
715
|
+
const action = updateAction(release, { installMode, webOrigin });
|
|
716
|
+
result.action = action;
|
|
717
|
+
if (action.command) result.recoveryCommand = action.command;
|
|
718
|
+
if (action.url) result.recoveryUrl = action.url;
|
|
719
|
+
|
|
720
|
+
const stateRoot = dependencies.stateRoot || defaultStateRoot();
|
|
721
|
+
const installerMarker = installMode === MACOS_INSTALLER_MODE
|
|
722
|
+
? dependencies.installerMarker || (await readInstallerMarker(packageRoot))
|
|
723
|
+
: null;
|
|
724
|
+
const installerAutomatic = Boolean(installerMarker?.teamId && releaseInfo?.artifact?.updater);
|
|
725
|
+
if (installMode === MACOS_INSTALLER_MODE) {
|
|
726
|
+
logger(`SIMY CLI ${release} is available for Apple Silicon.`);
|
|
727
|
+
logger(
|
|
728
|
+
change === "major"
|
|
729
|
+
? "Major installer updates require a manual signed installer download."
|
|
730
|
+
: installerAutomatic
|
|
731
|
+
? "The background agent will install it after active work is finished."
|
|
732
|
+
: "This release requires a manual installer download.",
|
|
733
|
+
);
|
|
734
|
+
logger(`Installer download: ${action.url}`);
|
|
735
|
+
return report(
|
|
736
|
+
mode === "update" ? "installer_update_deferred" : "update_available",
|
|
737
|
+
release,
|
|
738
|
+
);
|
|
739
|
+
}
|
|
530
740
|
|
|
531
741
|
if (installMode !== "global_npm") {
|
|
532
742
|
logger(`SIMY CLI ${release} is available. This ${installMode} installation is read-only; run: ${action.command}`);
|
|
@@ -548,12 +758,15 @@ export async function runCliUpdateGate({
|
|
|
548
758
|
|
|
549
759
|
if (change === "minor") logger(`Updating SIMY CLI ${info.version} → ${release}...`);
|
|
550
760
|
if (change === "major") logger(`Installing confirmed major SIMY CLI update ${info.version} → ${release}...`);
|
|
551
|
-
const stateRoot = dependencies.stateRoot || defaultStateRoot();
|
|
552
761
|
const acquire = dependencies.acquireUpdateLock || (() => acquireGlobalUpdateLock(stateRoot, dependencies));
|
|
762
|
+
const installVersion = dependencies.installVersion ||
|
|
763
|
+
((version) => installGlobalNpmVersion(version, { execFileCommand: dependencies.execFileCommand }));
|
|
764
|
+
const readInstalledVersion = dependencies.readInstalledVersion ||
|
|
765
|
+
(() => readGlobalNpmVersion({ execFileCommand: dependencies.execFileCommand }));
|
|
553
766
|
let releaseLock = await acquire();
|
|
554
767
|
if (!releaseLock) {
|
|
555
768
|
const installed = await waitForInstalledVersion(release, {
|
|
556
|
-
readInstalledVersion
|
|
769
|
+
readInstalledVersion,
|
|
557
770
|
timeoutMs: dependencies.lockWaitTimeoutMs,
|
|
558
771
|
pollMs: dependencies.lockPollMs,
|
|
559
772
|
});
|
|
@@ -566,8 +779,8 @@ export async function runCliUpdateGate({
|
|
|
566
779
|
await installAndVerifyVersion({
|
|
567
780
|
currentVersion: info.version,
|
|
568
781
|
targetVersion: release,
|
|
569
|
-
installVersion
|
|
570
|
-
readInstalledVersion
|
|
782
|
+
installVersion,
|
|
783
|
+
readInstalledVersion,
|
|
571
784
|
});
|
|
572
785
|
} catch (error) {
|
|
573
786
|
errorLogger(`SIMY update failed: ${errorMessage(error)}`);
|
|
@@ -589,12 +802,15 @@ export async function runCliUpdateGate({
|
|
|
589
802
|
if (incompatibleRetry) childEnv[INCOMPATIBLE_RETRY_ENV] = "1";
|
|
590
803
|
let exitCode;
|
|
591
804
|
try {
|
|
592
|
-
exitCode = await reexecute({
|
|
805
|
+
exitCode = await reexecute({
|
|
806
|
+
entryPath,
|
|
807
|
+
argv,
|
|
808
|
+
env: childEnv,
|
|
809
|
+
spawnProcess: dependencies.spawnProcess,
|
|
810
|
+
});
|
|
593
811
|
} catch (error) {
|
|
594
812
|
let rollbackDetail = `Recovery: npm install -g ${CLI_PACKAGE_NAME}@${info.version}`;
|
|
595
813
|
try {
|
|
596
|
-
const installVersion = dependencies.installVersion || ((version) => installGlobalNpmVersion(version, { execFileCommand: dependencies.execFileCommand }));
|
|
597
|
-
const readInstalledVersion = dependencies.readInstalledVersion || (() => readGlobalNpmVersion({ execFileCommand: dependencies.execFileCommand }));
|
|
598
814
|
await installVersion(info.version);
|
|
599
815
|
const restored = await readInstalledVersion();
|
|
600
816
|
if (restored !== info.version) throw new Error(`installed version is ${restored || "unknown"}`);
|
|
@@ -616,12 +832,13 @@ export async function runCliUpdateGate({
|
|
|
616
832
|
|
|
617
833
|
export async function reexecuteCliCommand({
|
|
618
834
|
entryPath,
|
|
835
|
+
executablePath = process.execPath,
|
|
619
836
|
argv,
|
|
620
837
|
env = process.env,
|
|
621
838
|
spawnProcess = spawn,
|
|
622
839
|
} = {}) {
|
|
623
840
|
return new Promise((resolve, reject) => {
|
|
624
|
-
const child = spawnProcess(
|
|
841
|
+
const child = spawnProcess(executablePath, [entryPath, ...argv], {
|
|
625
842
|
env,
|
|
626
843
|
stdio: "inherit",
|
|
627
844
|
});
|
|
@@ -683,8 +900,11 @@ export function shouldRetryIncompatibleCli(error, env = process.env) {
|
|
|
683
900
|
|
|
684
901
|
export async function restartDaemonProcess({
|
|
685
902
|
entryPath,
|
|
903
|
+
executablePath = process.execPath,
|
|
686
904
|
argv,
|
|
687
905
|
targetVersion,
|
|
906
|
+
expectedWebOrigin = null,
|
|
907
|
+
expectedInstallMode = null,
|
|
688
908
|
stateRoot = defaultStateRoot(),
|
|
689
909
|
spawnProcess = spawn,
|
|
690
910
|
timeoutMs = RESTART_HANDOFF_TIMEOUT_MS,
|
|
@@ -694,7 +914,7 @@ export async function restartDaemonProcess({
|
|
|
694
914
|
await mkdir(handoffDirectory, { recursive: true, mode: 0o700 });
|
|
695
915
|
const nonce = randomBytes(24).toString("hex");
|
|
696
916
|
const handoffPath = path.join(handoffDirectory, `${process.pid}-${nonce}.json`);
|
|
697
|
-
const child = spawnProcess(
|
|
917
|
+
const child = spawnProcess(executablePath, [entryPath, ...argv], {
|
|
698
918
|
detached: true,
|
|
699
919
|
stdio: "ignore",
|
|
700
920
|
env: {
|
|
@@ -719,7 +939,16 @@ export async function restartDaemonProcess({
|
|
|
719
939
|
`Restarted SIMY reported version ${handoff.version || "unknown"}, expected ${targetVersion}.`,
|
|
720
940
|
);
|
|
721
941
|
}
|
|
942
|
+
if (expectedWebOrigin && handoff.web_origin !== expectedWebOrigin) {
|
|
943
|
+
throw new Error("Restarted SIMY reported a different Web origin.");
|
|
944
|
+
}
|
|
945
|
+
if (expectedInstallMode && handoff.install_mode !== expectedInstallMode) {
|
|
946
|
+
throw new Error("Restarted SIMY reported a different installation channel.");
|
|
947
|
+
}
|
|
722
948
|
return handoff;
|
|
949
|
+
} catch (error) {
|
|
950
|
+
await terminateCandidate(child);
|
|
951
|
+
throw error;
|
|
723
952
|
} finally {
|
|
724
953
|
await rm(handoffPath, { force: true });
|
|
725
954
|
}
|
|
@@ -728,6 +957,8 @@ export async function restartDaemonProcess({
|
|
|
728
957
|
export async function writeUpdateHandoffFromEnvironment({
|
|
729
958
|
version,
|
|
730
959
|
port,
|
|
960
|
+
webOrigin = null,
|
|
961
|
+
installMode = null,
|
|
731
962
|
env = process.env,
|
|
732
963
|
stateRoot = defaultStateRoot(),
|
|
733
964
|
} = {}) {
|
|
@@ -741,13 +972,28 @@ export async function writeUpdateHandoffFromEnvironment({
|
|
|
741
972
|
await mkdir(handoffDirectory, { recursive: true, mode: 0o700 });
|
|
742
973
|
await writeFile(
|
|
743
974
|
handoffPath,
|
|
744
|
-
`${JSON.stringify({
|
|
975
|
+
`${JSON.stringify({
|
|
976
|
+
nonce,
|
|
977
|
+
version,
|
|
978
|
+
port,
|
|
979
|
+
pid: process.pid,
|
|
980
|
+
web_origin: webOrigin,
|
|
981
|
+
install_mode: installMode,
|
|
982
|
+
ready_at: new Date().toISOString(),
|
|
983
|
+
})}\n`,
|
|
745
984
|
{ encoding: "utf8", mode: 0o600 },
|
|
746
985
|
);
|
|
747
986
|
return true;
|
|
748
987
|
}
|
|
749
988
|
|
|
750
|
-
export function autoUpdateHelpText() {
|
|
989
|
+
export function autoUpdateHelpText({ installMode = null, webOrigin = null } = {}) {
|
|
990
|
+
if (installMode === MACOS_INSTALLER_MODE) {
|
|
991
|
+
return [
|
|
992
|
+
"Apple Silicon installer updates:",
|
|
993
|
+
" Signed patch and minor updates install in the background after active work finishes.",
|
|
994
|
+
` Major updates use the signed installer: ${new URL("/local-cli/download", webOrigin)}`,
|
|
995
|
+
].join("\n");
|
|
996
|
+
}
|
|
751
997
|
return [
|
|
752
998
|
"Recommended installation (enables verified CLI auto-update):",
|
|
753
999
|
` npm install -g ${CLI_PACKAGE_NAME}`,
|
|
@@ -762,6 +1008,9 @@ function manualUpdateMessage({
|
|
|
762
1008
|
daemon,
|
|
763
1009
|
requestedPort,
|
|
764
1010
|
}) {
|
|
1011
|
+
if (installMode === MACOS_INSTALLER_MODE) {
|
|
1012
|
+
return `SIMY CLI ${targetVersion} is available. Download the signed Apple Silicon installer; your tasks and settings will be preserved.`;
|
|
1013
|
+
}
|
|
765
1014
|
if (installMode !== "global_npm") {
|
|
766
1015
|
return `SIMY CLI ${targetVersion} is available. Install the global version to enable automatic updates.`;
|
|
767
1016
|
}
|
|
@@ -774,7 +1023,14 @@ function manualUpdateMessage({
|
|
|
774
1023
|
return `SIMY CLI ${targetVersion} is available. This version change requires manual confirmation.`;
|
|
775
1024
|
}
|
|
776
1025
|
|
|
777
|
-
export function updateAction(version) {
|
|
1026
|
+
export function updateAction(version, { installMode = "global_npm", webOrigin } = {}) {
|
|
1027
|
+
if (installMode === MACOS_INSTALLER_MODE) {
|
|
1028
|
+
return {
|
|
1029
|
+
id: "download_macos_installer",
|
|
1030
|
+
label: "Download the Apple Silicon installer",
|
|
1031
|
+
url: new URL("/local-cli/download", webOrigin).toString(),
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
778
1034
|
return {
|
|
779
1035
|
id: "install_global_cli",
|
|
780
1036
|
label: "Install and restart SIMY",
|
|
@@ -790,7 +1046,10 @@ function parseStableVersion(value) {
|
|
|
790
1046
|
export async function acquireGlobalUpdateLock(stateRoot = defaultStateRoot(), dependencies = {}) {
|
|
791
1047
|
const now = dependencies.now || Date.now;
|
|
792
1048
|
const lockDirectory = path.join(stateRoot, "updates");
|
|
793
|
-
const
|
|
1049
|
+
const lockName = dependencies.installMode === MACOS_INSTALLER_MODE
|
|
1050
|
+
? "macos-installer-update.lock"
|
|
1051
|
+
: "global-npm-update.lock";
|
|
1052
|
+
const lockPath = path.join(lockDirectory, lockName);
|
|
794
1053
|
await mkdir(lockDirectory, { recursive: true, mode: 0o700 });
|
|
795
1054
|
|
|
796
1055
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
@@ -857,6 +1116,24 @@ async function waitForHandoff(handoffPath, nonce, timeoutMs) {
|
|
|
857
1116
|
throw new Error("The updated SIMY daemon did not become healthy in time.");
|
|
858
1117
|
}
|
|
859
1118
|
|
|
1119
|
+
async function terminateCandidate(child) {
|
|
1120
|
+
if (!child?.pid || child.exitCode != null || child.signalCode != null) return;
|
|
1121
|
+
let resolveExit;
|
|
1122
|
+
const exited = new Promise((resolve) => {
|
|
1123
|
+
resolveExit = resolve;
|
|
1124
|
+
child.once?.("exit", resolve);
|
|
1125
|
+
});
|
|
1126
|
+
child.kill?.("SIGTERM");
|
|
1127
|
+
if (await Promise.race([exited.then(() => true), delay(2_000, false)])) return;
|
|
1128
|
+
child.kill?.("SIGKILL");
|
|
1129
|
+
await Promise.race([exited, delay(2_000)]);
|
|
1130
|
+
resolveExit?.();
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function delay(milliseconds, value = undefined) {
|
|
1134
|
+
return new Promise((resolve) => setTimeout(() => resolve(value), milliseconds));
|
|
1135
|
+
}
|
|
1136
|
+
|
|
860
1137
|
async function realpathOrResolve(value) {
|
|
861
1138
|
try {
|
|
862
1139
|
return await realpath(value);
|
package/src/browser.js
CHANGED
|
@@ -8,14 +8,31 @@ export async function openAuthorizationUrl(
|
|
|
8
8
|
logger = console,
|
|
9
9
|
} = {},
|
|
10
10
|
) {
|
|
11
|
-
|
|
11
|
+
return openExternalUrl(authorizationUrl, {
|
|
12
|
+
platform,
|
|
13
|
+
spawn,
|
|
14
|
+
logger,
|
|
15
|
+
description: "SIMY CLI authorization page",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function openExternalUrl(
|
|
20
|
+
url,
|
|
21
|
+
{
|
|
22
|
+
platform = process.platform,
|
|
23
|
+
spawn = spawnProcess,
|
|
24
|
+
logger = console,
|
|
25
|
+
description = "page",
|
|
26
|
+
} = {},
|
|
27
|
+
) {
|
|
28
|
+
if (!url) return { opened: false, reason: "not_required" };
|
|
12
29
|
|
|
13
|
-
const target = browserCommand(platform,
|
|
14
|
-
logger.log(`
|
|
30
|
+
const target = browserCommand(platform, url);
|
|
31
|
+
logger.log(`Open ${description}: ${url}`);
|
|
15
32
|
|
|
16
33
|
if (!target) {
|
|
17
34
|
logger.error(`SIMY could not open a browser on platform ${platform}.`);
|
|
18
|
-
logger.error(`Open this URL manually: ${
|
|
35
|
+
logger.error(`Open this URL manually: ${url}`);
|
|
19
36
|
return { opened: false, reason: "unsupported_platform" };
|
|
20
37
|
}
|
|
21
38
|
|
|
@@ -26,11 +43,11 @@ export async function openAuthorizationUrl(
|
|
|
26
43
|
});
|
|
27
44
|
await waitForSpawn(child);
|
|
28
45
|
child.unref();
|
|
29
|
-
logger.log(
|
|
46
|
+
logger.log(`Opened the ${description} in your browser.`);
|
|
30
47
|
return { opened: true };
|
|
31
48
|
} catch (error) {
|
|
32
|
-
logger.error(`SIMY could not open the
|
|
33
|
-
logger.error(`Open this URL manually: ${
|
|
49
|
+
logger.error(`SIMY could not open the ${description}: ${errorMessage(error)}`);
|
|
50
|
+
logger.error(`Open this URL manually: ${url}`);
|
|
34
51
|
return { opened: false, reason: "launch_failed", error };
|
|
35
52
|
}
|
|
36
53
|
}
|
package/src/cli-contract.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./durable-local-task-steps-contract.js";
|
|
14
14
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
15
|
|
|
16
|
-
export const CLI_VERSION = "0.5.
|
|
16
|
+
export const CLI_VERSION = "0.5.10";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|