@synap-core/cli 1.13.1-next.20261102.ec3d166 → 1.13.1-next.20261102.f2ce919
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/dist/build-info.json +2 -2
- package/dist/commands/context.js +2 -1
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/knowledge.d.ts +36 -5
- package/dist/commands/knowledge.js +82 -27
- package/dist/commands/knowledge.js.map +1 -1
- package/dist/commands/market.d.ts +54 -53
- package/dist/commands/market.js +144 -209
- package/dist/commands/market.js.map +1 -1
- package/dist/commands/mcp.js +6 -13
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/worker.d.ts +24 -2
- package/dist/commands/worker.js +97 -4
- package/dist/commands/worker.js.map +1 -1
- package/dist/index.js +15 -16
- package/dist/index.js.map +1 -1
- package/dist/lib/credential-class.d.ts +8 -0
- package/dist/lib/credential-class.js +11 -0
- package/dist/lib/credential-class.js.map +1 -1
- package/dist/lib/hub-client.js +11 -29
- package/dist/lib/hub-client.js.map +1 -1
- package/dist/lib/template-lifecycle.d.ts +0 -31
- package/dist/lib/template-lifecycle.js +0 -74
- package/dist/lib/template-lifecycle.js.map +1 -1
- package/package.json +1 -1
- package/skills/synap/SKILL.md +11 -7
- package/skills/synap/concepts.md +1 -1
- package/skills/synap/hand-off.md +10 -6
package/dist/commands/market.js
CHANGED
|
@@ -40,7 +40,7 @@ import { bundledTemplatesVersion } from "../lib/bundle-version.js";
|
|
|
40
40
|
import { HubNetworkError } from "../lib/hub-client.js";
|
|
41
41
|
import { checksFromStandings, fetchPackageStandings, StandingsUnavailableError, } from "../lib/package-updates.js";
|
|
42
42
|
import { podAdminOrigin } from "../lib/safe-open.js";
|
|
43
|
-
import { parseDataChanges,
|
|
43
|
+
import { parseDataChanges, printDataChanges, parseAcceptList, decideConsent, consentRerunCommand, lifecycleNotice, overlayNotice, overlayNeedsAttention, } from "../lib/template-lifecycle.js";
|
|
44
44
|
// Same longer budget launch uses — an apply writes profiles/views/entities.
|
|
45
45
|
const APPLY_TIMEOUT_MS = 120_000;
|
|
46
46
|
/** `--timeout <seconds>` → ms, falling back to `APPLY_TIMEOUT_MS` when unset/invalid. */
|
|
@@ -385,8 +385,8 @@ function printSeedOutcomes(dependencies, seedSummary) {
|
|
|
385
385
|
log.hint(`${d.slug}: failed${d.seedOutcome.error ? ` (${d.seedOutcome.error})` : ""}`);
|
|
386
386
|
}
|
|
387
387
|
}
|
|
388
|
-
// ── Apply-outcome honesty —
|
|
389
|
-
//
|
|
388
|
+
// ── Apply-outcome honesty — `marketInstall`'s reading of `/packages/apply` ───
|
|
389
|
+
// Every install reads the same `/packages/apply` answer and must render the SAME honest
|
|
390
390
|
// verdict from its response: a partial capability-seed failure, a version
|
|
391
391
|
// stamp that silently didn't take, or a compose-overlay result are never a
|
|
392
392
|
// plain "✓ success" — in the human text OR in `--json`.
|
|
@@ -439,8 +439,8 @@ export function podApplyState(res) {
|
|
|
439
439
|
return null;
|
|
440
440
|
}
|
|
441
441
|
/**
|
|
442
|
-
* The verdict of one `/packages/apply` response — the ONE reader
|
|
443
|
-
* `marketInstall`
|
|
442
|
+
* The verdict of one `/packages/apply` response — the ONE reader
|
|
443
|
+
* `marketInstall` calls. Layer 2's outcome comes from the
|
|
444
444
|
* pod's own state (`podApplyState`): an apply the pod says is unfinished is
|
|
445
445
|
* never "updated", and never a stamp contradiction (the pod held the version
|
|
446
446
|
* on purpose — redeploying fixes nothing).
|
|
@@ -459,12 +459,11 @@ export function classifyApplyResponse(entry, res, ctx) {
|
|
|
459
459
|
return classifyApplyResult(entry, res.workspace, res.dependencies, res.seedSummary, ctx);
|
|
460
460
|
}
|
|
461
461
|
/**
|
|
462
|
-
* Classify one `/packages/apply` response into the honest verdict
|
|
463
|
-
* `marketInstall`
|
|
462
|
+
* Classify one `/packages/apply` response into the honest verdict
|
|
463
|
+
* `marketInstall` prints (and puts in `--json`).
|
|
464
464
|
* `wasUnstamped` = the slug was already installed with no recorded version
|
|
465
465
|
* BEFORE this call (only `marketInstall` can detect this today — see its
|
|
466
|
-
* `fetchInstalledTemplates` call
|
|
467
|
-
* already-versioned slugs, so it always passes `false`).
|
|
466
|
+
* `fetchInstalledTemplates` call).
|
|
468
467
|
*/
|
|
469
468
|
export function classifyApplyResult(entry, ws, dependencies, seedSummary, ctx) {
|
|
470
469
|
// A post-workspace LAYER failure counts the same as a seed failure: the
|
|
@@ -519,7 +518,7 @@ export function deferredPlacementLines(res) {
|
|
|
519
518
|
export function heldVersionLine(res) {
|
|
520
519
|
return typeof res.templateApplyWarning === "string" ? res.templateApplyWarning : null;
|
|
521
520
|
}
|
|
522
|
-
/** Render a `classifyApplyResult` verdict — the ONE honest-text door
|
|
521
|
+
/** Render a `classifyApplyResult` verdict — the ONE honest-text door of `marketInstall`. */
|
|
523
522
|
export function printApplyVerdict(entry, verdict) {
|
|
524
523
|
switch (verdict.status) {
|
|
525
524
|
case "installed":
|
|
@@ -1263,24 +1262,49 @@ function stampVersion(pkg, slug, version) {
|
|
|
1263
1262
|
return { ...pkg, _meta: { ...meta, slug, ...(version ? { version } : {}) } };
|
|
1264
1263
|
}
|
|
1265
1264
|
/**
|
|
1266
|
-
* The slugs with an update that `synap market update <slug>` can apply
|
|
1267
|
-
* kind
|
|
1268
|
-
*
|
|
1265
|
+
* The slugs with an update that `synap market update <slug>` can apply. Every
|
|
1266
|
+
* installed kind (space, capability, view, skill, automation) updates through
|
|
1267
|
+
* the pod's ONE update door (`POST /package-updates/{slug}/apply`), so every
|
|
1268
|
+
* slug with an update is offered; what only the person can do is said by the
|
|
1269
|
+
* pod at apply time (`NEEDS_PERSON`), never guessed here. Next-step
|
|
1270
|
+
* suggestions use it.
|
|
1269
1271
|
*/
|
|
1270
1272
|
export function cliUpdatableSlugs(checks) {
|
|
1271
|
-
return checks.filter((c) => c.updateAvailable
|
|
1273
|
+
return checks.filter((c) => c.updateAvailable).map((c) => c.slug);
|
|
1272
1274
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1275
|
+
/** The status of an update left for the person to apply in pod admin (not a failure). */
|
|
1276
|
+
export const APPLY_IN_POD_ADMIN = "apply-in-pod-admin";
|
|
1277
|
+
/**
|
|
1278
|
+
* Where the person finishes ONE package's update: pod admin's review page for
|
|
1279
|
+
* it (`/marketplace/updates/<slug>`), when the pod-admin origin derives from
|
|
1280
|
+
* the pod's; else the pod's own link (`reviewUrl`, pod-relative
|
|
1281
|
+
* `/open/update/<slug>`, which the pod sends on to pod admin); else the
|
|
1282
|
+
* Updates tab's words.
|
|
1283
|
+
*/
|
|
1284
|
+
export function packageReviewLink(podUrl, slug, reviewUrl) {
|
|
1285
|
+
const admin = podAdminOrigin(podUrl);
|
|
1286
|
+
if (admin)
|
|
1287
|
+
return `${admin}/marketplace/updates/${encodeURIComponent(slug)}`;
|
|
1288
|
+
if (typeof reviewUrl === "string" && reviewUrl.startsWith("/"))
|
|
1289
|
+
return `${podUrl.replace(/\/+$/, "")}${reviewUrl}`;
|
|
1290
|
+
if (typeof reviewUrl === "string" && /^https?:\/\//.test(reviewUrl))
|
|
1291
|
+
return reviewUrl;
|
|
1292
|
+
return undefined;
|
|
1293
|
+
}
|
|
1294
|
+
/** The one row for an update only the person can finish: on pod admin, not a failure. */
|
|
1295
|
+
function applyInPodAdminResult(c, podUrl, json, pod = {}) {
|
|
1296
|
+
const link = podUrl ? packageReviewLink(podUrl, c.slug, pod.reviewUrl) : undefined;
|
|
1297
|
+
if (!json) {
|
|
1298
|
+
log.warn(`${c.slug} is a ${c.kind ?? "workspace"} — to apply in pod admin${pod.why ? `: ${pod.why}` : "."} Nothing was written.`);
|
|
1299
|
+
log.hint(link ? `Apply it yourself: ${link}` : podUrl ? marketUpdatesDoor(podUrl) : "Apply it yourself from pod admin → Marketplace → Updates.");
|
|
1283
1300
|
}
|
|
1301
|
+
return {
|
|
1302
|
+
slug: c.slug,
|
|
1303
|
+
outcome: APPLY_IN_POD_ADMIN,
|
|
1304
|
+
status: APPLY_IN_POD_ADMIN,
|
|
1305
|
+
warnings: [],
|
|
1306
|
+
...(link ? { reviewUrl: link } : {}),
|
|
1307
|
+
};
|
|
1284
1308
|
}
|
|
1285
1309
|
/**
|
|
1286
1310
|
* Per-installed-slug drift check. SERVER-FIRST: when the pod runs the
|
|
@@ -1345,124 +1369,111 @@ export function dedupeBySlug(checks) {
|
|
|
1345
1369
|
const seen = new Set();
|
|
1346
1370
|
return checks.filter((c) => (seen.has(c.slug) ? false : (seen.add(c.slug), true)));
|
|
1347
1371
|
}
|
|
1372
|
+
/** The pod's typed refusals of an update (`services/package-updates/refusals.ts`). */
|
|
1373
|
+
const NEEDS_THE_PERSON = new Set(["NEEDS_PERSON", "CHOICES_NEEDED"]);
|
|
1348
1374
|
/**
|
|
1349
|
-
* Apply
|
|
1350
|
-
*
|
|
1351
|
-
*
|
|
1352
|
-
*
|
|
1375
|
+
* Apply ONE installed package's update through the pod's ONE update door,
|
|
1376
|
+
* `POST /package-updates/{slug}/apply`, whatever its kind (space, capability,
|
|
1377
|
+
* view, skill, automation) and whoever holds the key. The pod applies the
|
|
1378
|
+
* rule (backend `services/package-updates/`): a person's apply lands; an
|
|
1379
|
+
* agent key's is filed for the person's approval (`proposed`) and lands when
|
|
1380
|
+
* they approve it; what only the person can do (agree to changes to existing
|
|
1381
|
+
* records, choose for a field both sides changed, change a view) is
|
|
1382
|
+
* `NEEDS_PERSON` / `CHOICES_NEEDED` with that package's review page. An
|
|
1383
|
+
* older pod that refuses an agent here (403, no `reasonCode`) keeps today's
|
|
1384
|
+
* line: to apply in pod admin.
|
|
1353
1385
|
*/
|
|
1354
|
-
export async function
|
|
1355
|
-
if (
|
|
1356
|
-
const
|
|
1386
|
+
export async function applyUpdateThroughDoor(c, cfg, opts, applyTimeoutMs = APPLY_TIMEOUT_MS, acceptDataChanges = []) {
|
|
1387
|
+
if (!c.latestVersion) {
|
|
1388
|
+
const error = "the pod named no catalog version to update to";
|
|
1357
1389
|
if (!opts.json)
|
|
1358
|
-
log.
|
|
1359
|
-
return { slug:
|
|
1360
|
-
}
|
|
1361
|
-
const cpToken = getStoredToken()?.token;
|
|
1362
|
-
// Same honest payload as `marketInstall` (see `resolveInstallPayload`).
|
|
1363
|
-
const payload = extra.payload !== undefined ? extra.payload : await resolveInstallPayload(entry.slug, cat, cpToken);
|
|
1364
|
-
if (!payload) {
|
|
1365
|
-
if (!opts.json) {
|
|
1366
|
-
log.error(`Couldn't fetch "${entry.name}"'s definition from the control plane.`);
|
|
1367
|
-
if (!cat.loggedIn)
|
|
1368
|
-
log.hint("It may be private — try: synap login");
|
|
1369
|
-
}
|
|
1370
|
-
return { slug: entry.slug, outcome: "fetch-failed", status: "fetch-failed", warnings: [] };
|
|
1390
|
+
log.error(`${c.slug} not updated — ${error}.`);
|
|
1391
|
+
return { slug: c.slug, outcome: "failed", status: "failed", warnings: [], error };
|
|
1371
1392
|
}
|
|
1372
|
-
const
|
|
1373
|
-
|
|
1374
|
-
:
|
|
1375
|
-
|
|
1393
|
+
const body = {
|
|
1394
|
+
expectedVersion: c.latestVersion,
|
|
1395
|
+
choices: {},
|
|
1396
|
+
...(acceptDataChanges.length > 0 ? { acceptDataChanges } : {}),
|
|
1397
|
+
};
|
|
1398
|
+
const s = opts.json ? null : ora(`Updating ${c.slug}…`).start();
|
|
1376
1399
|
try {
|
|
1377
|
-
const res = (await hubPost(
|
|
1400
|
+
const res = (await hubPost(`/package-updates/${encodeURIComponent(c.slug)}/apply`, body, cfg, applyTimeoutMs));
|
|
1378
1401
|
s?.stop();
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
// Waiting, never failed — it applies when the person approves it.
|
|
1383
|
-
if (writeGovernance(res) === "proposed") {
|
|
1384
|
-
const proposalId = res.proposalId ? String(res.proposalId) : undefined;
|
|
1385
|
-
const reviewUrl = approvalLink(cfg.podUrl, proposalId, res.reviewUrl);
|
|
1402
|
+
if (res?.status === "proposed") {
|
|
1403
|
+
const proposalIds = Array.isArray(res.proposalIds) ? res.proposalIds.map(String) : [];
|
|
1404
|
+
const links = proposalIds.map((id) => approvalLink(cfg.podUrl, id, undefined) ?? `${cfg.podUrl.replace(/\/+$/, "")}/open/${encodeURIComponent(id)}`);
|
|
1386
1405
|
if (!opts.json) {
|
|
1387
|
-
log.info(`${
|
|
1388
|
-
|
|
1389
|
-
log.hint(`Approve it: ${
|
|
1390
|
-
else if (proposalId)
|
|
1391
|
-
log.hint(`Approve it: synap open proposal ${proposalId}`);
|
|
1406
|
+
log.info(`${c.slug} — update filed for your approval (not applied yet).`);
|
|
1407
|
+
for (const link of links)
|
|
1408
|
+
log.hint(`Approve it: ${link}`);
|
|
1392
1409
|
}
|
|
1393
1410
|
return {
|
|
1394
|
-
slug:
|
|
1411
|
+
slug: c.slug,
|
|
1395
1412
|
outcome: "proposed",
|
|
1396
1413
|
status: "proposed",
|
|
1397
1414
|
warnings: [],
|
|
1398
|
-
...(
|
|
1399
|
-
...(
|
|
1415
|
+
...(proposalIds[0] ? { proposalId: proposalIds[0], proposalIds } : {}),
|
|
1416
|
+
...(links[0] ? { reviewUrl: links[0] } : {}),
|
|
1400
1417
|
};
|
|
1401
1418
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
printApplyVerdict(entry, verdict);
|
|
1416
|
-
printSeedOutcomes(dependencies, seedSummary);
|
|
1417
|
-
printLayerOutcomes(ws?.layers);
|
|
1418
|
-
for (const line of deferredPlacementLines(res))
|
|
1419
|
-
log.info(line);
|
|
1419
|
+
if (res?.status === "applied") {
|
|
1420
|
+
const warning = typeof res.warning === "string" && res.warning ? res.warning : undefined;
|
|
1421
|
+
const receipt = res.receipt;
|
|
1422
|
+
// A field update whose every field stayed yours changed nothing.
|
|
1423
|
+
const unchanged = !!receipt && Array.isArray(receipt.changes) && receipt.changes.length === 0;
|
|
1424
|
+
const status = warning ? "updated-with-failures" : unchanged ? "unchanged" : "updated";
|
|
1425
|
+
if (!opts.json) {
|
|
1426
|
+
if (warning)
|
|
1427
|
+
log.warn(`${c.slug} — updated, not finished: ${warning}`);
|
|
1428
|
+
else
|
|
1429
|
+
log.success(`${c.slug} — ${status}`);
|
|
1430
|
+
}
|
|
1431
|
+
return { slug: c.slug, outcome: status, status, warnings: warning ? [warning] : [] };
|
|
1420
1432
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
warnings: verdict.warnings,
|
|
1426
|
-
dependencies,
|
|
1427
|
-
seedSummary,
|
|
1428
|
-
};
|
|
1433
|
+
const error = "the pod answered with a result this CLI can't read — not assuming it updated";
|
|
1434
|
+
if (!opts.json)
|
|
1435
|
+
log.error(`${c.slug} — ${error}`);
|
|
1436
|
+
return { slug: c.slug, outcome: "failed", status: "failed", warnings: [], error };
|
|
1429
1437
|
}
|
|
1430
1438
|
catch (e) {
|
|
1431
1439
|
s?.stop();
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
const
|
|
1435
|
-
if (
|
|
1440
|
+
const refusal = e instanceof HubError ? (e.body ?? {}) : {};
|
|
1441
|
+
const reasonCode = typeof refusal.reasonCode === "string" ? refusal.reasonCode : undefined;
|
|
1442
|
+
const said = e instanceof HubError ? (e.detail ?? e.message) : e instanceof Error ? e.message : String(e);
|
|
1443
|
+
if (reasonCode && NEEDS_THE_PERSON.has(reasonCode)) {
|
|
1444
|
+
return applyInPodAdminResult(c, cfg.podUrl, opts.json, { why: said, reviewUrl: refusal.reviewUrl });
|
|
1445
|
+
}
|
|
1446
|
+
// An older pod refuses an agent's space or capability update here: today's line.
|
|
1447
|
+
if (e instanceof HubError && e.status === 403 && !reasonCode && /\bagent\b/i.test(said)) {
|
|
1448
|
+
return applyInPodAdminResult(c, cfg.podUrl, opts.json);
|
|
1449
|
+
}
|
|
1450
|
+
if (reasonCode === "INVALID_CATALOG_DEFINITION") {
|
|
1451
|
+
const fieldErrors = (refusal.fieldErrors ?? undefined);
|
|
1436
1452
|
if (!opts.json) {
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
for (const hint of said.hints)
|
|
1441
|
-
log.hint(hint);
|
|
1453
|
+
log.error(`${c.slug} not updated — ${said}`);
|
|
1454
|
+
for (const [field, messages] of Object.entries(fieldErrors ?? {}))
|
|
1455
|
+
log.dim(` ${field || "(the definition)"}: ${messages.join("; ")}`);
|
|
1442
1456
|
}
|
|
1443
1457
|
return {
|
|
1444
|
-
slug:
|
|
1445
|
-
outcome: "
|
|
1446
|
-
status: "
|
|
1458
|
+
slug: c.slug,
|
|
1459
|
+
outcome: "failed",
|
|
1460
|
+
status: "failed",
|
|
1447
1461
|
warnings: [],
|
|
1448
|
-
|
|
1462
|
+
error: said,
|
|
1463
|
+
...(fieldErrors ? { fieldErrors } : {}),
|
|
1449
1464
|
};
|
|
1450
1465
|
}
|
|
1451
1466
|
const timedOut = e instanceof HubNetworkError && e.code === "TIMEOUT";
|
|
1452
|
-
const refusal = dataChangeRefusalMessage(e);
|
|
1453
1467
|
if (!opts.json) {
|
|
1454
|
-
log.error(`${
|
|
1455
|
-
|
|
1456
|
-
log.warn(refusal);
|
|
1457
|
-
else
|
|
1458
|
-
renderHubError(e, { personDoor: marketUpdatesDoor(cfg.podUrl) });
|
|
1468
|
+
log.error(`${c.slug} update failed`);
|
|
1469
|
+
renderHubError(e, { personDoor: marketUpdatesDoor(cfg.podUrl) });
|
|
1459
1470
|
}
|
|
1460
1471
|
return {
|
|
1461
|
-
slug:
|
|
1472
|
+
slug: c.slug,
|
|
1462
1473
|
outcome: timedOut ? "timed-out" : "failed",
|
|
1463
1474
|
status: timedOut ? "timed-out" : "failed",
|
|
1464
1475
|
warnings: [],
|
|
1465
|
-
error:
|
|
1476
|
+
error: said,
|
|
1466
1477
|
};
|
|
1467
1478
|
}
|
|
1468
1479
|
}
|
|
@@ -1506,54 +1517,6 @@ async function previewDataChanges(entry, cat, cfg) {
|
|
|
1506
1517
|
return { payload, error: `the pod's preview failed: ${status}` };
|
|
1507
1518
|
}
|
|
1508
1519
|
}
|
|
1509
|
-
/** One slug's outcome out of the shared pod-wide config reconcile. */
|
|
1510
|
-
async function reconcileOutcome(c, report, opts) {
|
|
1511
|
-
let r;
|
|
1512
|
-
try {
|
|
1513
|
-
r = await report;
|
|
1514
|
-
}
|
|
1515
|
-
catch (e) {
|
|
1516
|
-
if (!opts.json) {
|
|
1517
|
-
log.error(`${c.slug} update failed`);
|
|
1518
|
-
renderHubError(e);
|
|
1519
|
-
}
|
|
1520
|
-
return { slug: c.slug, outcome: "failed", status: "failed", warnings: [], error: e instanceof Error ? e.message : String(e) };
|
|
1521
|
-
}
|
|
1522
|
-
// A report this CLI cannot read is a FAILED read — never "up to date".
|
|
1523
|
-
const lists = [r?.updated, r?.ownerOwnedSkipped, r?.skipped, r?.conflicts];
|
|
1524
|
-
if (!lists.some(Array.isArray) || lists.some((l) => l !== undefined && !Array.isArray(l))) {
|
|
1525
|
-
const error = "the pod's reconcile answered with a report this CLI can't read — not assuming it updated";
|
|
1526
|
-
if (!opts.json)
|
|
1527
|
-
log.error(`${c.slug} — ${error}`);
|
|
1528
|
-
return { slug: c.slug, outcome: "failed", status: "failed", warnings: [], error };
|
|
1529
|
-
}
|
|
1530
|
-
const mine = (rows) => (rows ?? []).filter((e) => e.packageSlug === c.slug);
|
|
1531
|
-
const conflicts = mine(r.conflicts);
|
|
1532
|
-
const owned = mine(r.ownerOwnedSkipped);
|
|
1533
|
-
const updated = mine(r.updated);
|
|
1534
|
-
const warnings = owned.map((e) => `${e.name}: kept your edits — ${e.reason}`);
|
|
1535
|
-
if (conflicts.length > 0) {
|
|
1536
|
-
const error = conflicts.map((e) => `${e.name}: ${e.reason}`).join("; ");
|
|
1537
|
-
if (!opts.json)
|
|
1538
|
-
log.error(`${c.slug} update failed — ${error}`);
|
|
1539
|
-
return { slug: c.slug, outcome: "failed", status: updated.length > 0 ? "updated-with-failures" : "failed", warnings, error };
|
|
1540
|
-
}
|
|
1541
|
-
if (!opts.json)
|
|
1542
|
-
for (const w of warnings)
|
|
1543
|
-
log.warn(`${c.slug} — ${w}`);
|
|
1544
|
-
if (updated.length > 0 || owned.length > 0) {
|
|
1545
|
-
// Every field it could advance did; a field you edited stays yours (warned above).
|
|
1546
|
-
const status = updated.length > 0 ? "updated" : "unchanged";
|
|
1547
|
-
if (!opts.json)
|
|
1548
|
-
log.success(`${c.slug} — ${status}`);
|
|
1549
|
-
return { slug: c.slug, outcome: status, status, warnings };
|
|
1550
|
-
}
|
|
1551
|
-
// Skipped (cache miss, unresolved definition…) or not reported at all: NOT updated — never "current".
|
|
1552
|
-
const error = mine(r.skipped).map((e) => `${e.name}: ${e.reason}`).join("; ") || "the pod's reconcile did not report this package";
|
|
1553
|
-
if (!opts.json)
|
|
1554
|
-
log.warn(`${c.slug} was not updated — ${error}`);
|
|
1555
|
-
return { slug: c.slug, outcome: "not-updated", status: "not-updated", warnings, error };
|
|
1556
|
-
}
|
|
1557
1520
|
export async function marketUpdate(slugsArg, opts) {
|
|
1558
1521
|
const applyTimeoutMs = parseApplyTimeoutMs(opts.timeout);
|
|
1559
1522
|
const cat = await buildMarketCatalog();
|
|
@@ -1666,8 +1629,9 @@ export async function marketUpdate(slugsArg, opts) {
|
|
|
1666
1629
|
process.exit(1);
|
|
1667
1630
|
}
|
|
1668
1631
|
for (const c of withUpdates) {
|
|
1669
|
-
//
|
|
1670
|
-
|
|
1632
|
+
// Only a space's update changes existing records: the preview of its
|
|
1633
|
+
// data changes is the template door's (`/packages/preflight`).
|
|
1634
|
+
if ((c.kind ?? "workspace") !== "workspace")
|
|
1671
1635
|
continue;
|
|
1672
1636
|
const entry = entryBySlug.get(c.slug);
|
|
1673
1637
|
if (entry)
|
|
@@ -1707,6 +1671,8 @@ export async function marketUpdate(slugsArg, opts) {
|
|
|
1707
1671
|
}
|
|
1708
1672
|
log.heading(" Installed packages");
|
|
1709
1673
|
log.blank();
|
|
1674
|
+
// The slug column fits the longest slug (capped), with at least 2 spaces after it.
|
|
1675
|
+
const slugW = Math.min(Math.max(28, ...checks.map((c) => c.slug.length + 2)), 46);
|
|
1710
1676
|
for (const c of checks) {
|
|
1711
1677
|
// ONE label door. This was a hand-rolled second copy that diverged from
|
|
1712
1678
|
// `installedStatusLabel` in two ways that both LIED: it had no cache-cold
|
|
@@ -1717,7 +1683,7 @@ export async function marketUpdate(slugsArg, opts) {
|
|
|
1717
1683
|
const label = retiredSlugs.has(c.slug)
|
|
1718
1684
|
? chalk.yellow("retired") + chalk.dim(` · synap templates retire ${c.slug}`)
|
|
1719
1685
|
: installedStatusLabel(c, entryBySlug.get(c.slug)?.isPrivate ?? false, cat.loggedIn);
|
|
1720
|
-
console.log(" " + chalk.cyan(c.slug.padEnd(
|
|
1686
|
+
console.log(" " + chalk.cyan(clip(c.slug, slugW - 2).padEnd(slugW)) + label);
|
|
1721
1687
|
}
|
|
1722
1688
|
log.blank();
|
|
1723
1689
|
if (noVersionInfo.length > 0) {
|
|
@@ -1764,10 +1730,9 @@ export async function marketUpdate(slugsArg, opts) {
|
|
|
1764
1730
|
return;
|
|
1765
1731
|
}
|
|
1766
1732
|
if (!shouldApply) {
|
|
1767
|
-
//
|
|
1768
|
-
//
|
|
1733
|
+
// Every update applies through the pod's one update door, so every one
|
|
1734
|
+
// is offered; what only the person can do is said when it is applied.
|
|
1769
1735
|
const steps = FLOW.afterMarketUpdateCheck(cliUpdatableSlugs(withUpdates));
|
|
1770
|
-
const personApplies = withUpdates.filter((c) => updateDoorFor(c.kind) === null).map((c) => c.slug);
|
|
1771
1736
|
if (opts.json) {
|
|
1772
1737
|
console.log(JSON.stringify({
|
|
1773
1738
|
checks,
|
|
@@ -1775,9 +1740,6 @@ export async function marketUpdate(slugsArg, opts) {
|
|
|
1775
1740
|
unknownSlugs,
|
|
1776
1741
|
applied: [],
|
|
1777
1742
|
nextSteps: steps,
|
|
1778
|
-
...(personApplies.length > 0
|
|
1779
|
-
? { appliedInPodAdmin: { slugs: personApplies, where: marketUpdatesDoor(cfg.podUrl) } }
|
|
1780
|
-
: {}),
|
|
1781
1743
|
dataChanges: dataChangesJson,
|
|
1782
1744
|
templateLifecycle: lifecycleJson,
|
|
1783
1745
|
}, null, 2));
|
|
@@ -1787,44 +1749,12 @@ export async function marketUpdate(slugsArg, opts) {
|
|
|
1787
1749
|
if ([...previews.values()].some((pv) => (pv.changes?.length ?? 0) > 0)) {
|
|
1788
1750
|
log.hint("Updates with data changes ask before they run. To agree up front: --accept-data-changes <opKey,…> (or --yes for all listed)");
|
|
1789
1751
|
}
|
|
1790
|
-
if (personApplies.length > 0) {
|
|
1791
|
-
log.hint(`${personApplies.join(", ")}: not updated by this command. ${marketUpdatesDoor(cfg.podUrl)}`);
|
|
1792
|
-
}
|
|
1793
1752
|
renderNextSteps(steps);
|
|
1794
1753
|
return;
|
|
1795
1754
|
}
|
|
1796
1755
|
const acceptList = parseAcceptList(opts.acceptDataChanges);
|
|
1797
1756
|
const results = [];
|
|
1798
|
-
// One pod-wide reconcile serves every view/skill/automation update — run it
|
|
1799
|
-
// at most once, lazily, and read each slug's outcome from its report.
|
|
1800
|
-
let configsReport;
|
|
1801
1757
|
for (const c of withUpdates) {
|
|
1802
|
-
const door = updateDoorFor(c.kind);
|
|
1803
|
-
if (door === null) {
|
|
1804
|
-
if (!opts.json) {
|
|
1805
|
-
// A capability update is a person's act on pod admin (the pod refuses
|
|
1806
|
-
// an agent key there); any other kind here has no updater at all.
|
|
1807
|
-
log.warn(c.kind === "capability"
|
|
1808
|
-
? `${c.slug} is a capability — it was not updated here. Nothing was written. ${marketUpdatesDoor(cfg.podUrl)}`
|
|
1809
|
-
: `${c.slug} is a ${c.kind} — the pod has no per-package update for this kind, so it was not updated. Nothing was written.`);
|
|
1810
|
-
}
|
|
1811
|
-
results.push({ slug: c.slug, outcome: "no-updater", status: "no-updater", warnings: [] });
|
|
1812
|
-
continue;
|
|
1813
|
-
}
|
|
1814
|
-
if (door === "configs-reconcile") {
|
|
1815
|
-
configsReport ??= hubPost("/configs/reconcile", {}, cfg, applyTimeoutMs);
|
|
1816
|
-
results.push(await reconcileOutcome(c, configsReport, { json: opts.json }));
|
|
1817
|
-
continue;
|
|
1818
|
-
}
|
|
1819
|
-
const entry = entryBySlug.get(c.slug);
|
|
1820
|
-
if (!entry) {
|
|
1821
|
-
// Absent from the catalog THIS CLI read — not proof it left the catalog
|
|
1822
|
-
// (that is the pod's `unlisted`, shown above, and only after a complete sync).
|
|
1823
|
-
if (!opts.json)
|
|
1824
|
-
log.warn(`Couldn't find "${c.slug}" in the catalog read just now — skipping.`);
|
|
1825
|
-
results.push({ slug: c.slug, outcome: "not-in-catalog", status: "not-in-catalog", warnings: [] });
|
|
1826
|
-
continue;
|
|
1827
|
-
}
|
|
1828
1758
|
const pv = previews.get(c.slug);
|
|
1829
1759
|
const pending = pv?.changes ?? [];
|
|
1830
1760
|
let accepted = [];
|
|
@@ -1849,17 +1779,14 @@ export async function marketUpdate(slugsArg, opts) {
|
|
|
1849
1779
|
}
|
|
1850
1780
|
if (missing.length > 0) {
|
|
1851
1781
|
if (!opts.json) {
|
|
1852
|
-
log.warn(`${
|
|
1782
|
+
log.warn(`${c.slug} not updated: you have not agreed to its data changes. Nothing was written.`);
|
|
1853
1783
|
log.hint(`To agree and update: ${consentRerunCommand(c.slug, missing)}`);
|
|
1854
1784
|
}
|
|
1855
1785
|
results.push({ slug: c.slug, outcome: "needs-consent", status: "needs-consent", warnings: [], pendingDataChanges: missing });
|
|
1856
1786
|
continue;
|
|
1857
1787
|
}
|
|
1858
1788
|
}
|
|
1859
|
-
results.push(await
|
|
1860
|
-
payload: pv?.payload,
|
|
1861
|
-
acceptDataChanges: accepted,
|
|
1862
|
-
}));
|
|
1789
|
+
results.push(await applyUpdateThroughDoor(c, cfg, { json: opts.json }, applyTimeoutMs, accepted));
|
|
1863
1790
|
}
|
|
1864
1791
|
if (results.some((r) => r.status === "needs-consent"))
|
|
1865
1792
|
process.exitCode = 1;
|
|
@@ -1883,8 +1810,8 @@ export async function marketUpdate(slugsArg, opts) {
|
|
|
1883
1810
|
renderNextSteps(steps);
|
|
1884
1811
|
}
|
|
1885
1812
|
/**
|
|
1886
|
-
* One-line rollup over a batch of `
|
|
1887
|
-
* the SAME per-item `status`
|
|
1813
|
+
* One-line rollup over a batch of update outcomes (`applyUpdateThroughDoor`) — computed from
|
|
1814
|
+
* the SAME per-item `status` the
|
|
1888
1815
|
* loop already collects, not a separate tally. `"installed"`/`"updated"`/
|
|
1889
1816
|
* `"composed"`/`"legacy-unknown"` count as a clean update, `"unchanged"` as
|
|
1890
1817
|
* already-current, the `*-with-failures` and `"stamp-contradiction"` statuses
|
|
@@ -1904,6 +1831,7 @@ export function summarizeApplyResults(results) {
|
|
|
1904
1831
|
let failed = 0;
|
|
1905
1832
|
let proposed = 0;
|
|
1906
1833
|
let needsConsent = 0;
|
|
1834
|
+
let toApplyInPodAdmin = 0;
|
|
1907
1835
|
for (const r of results) {
|
|
1908
1836
|
switch (r.status) {
|
|
1909
1837
|
case "installed":
|
|
@@ -1938,6 +1866,10 @@ export function summarizeApplyResults(results) {
|
|
|
1938
1866
|
case "needs-consent":
|
|
1939
1867
|
needsConsent++;
|
|
1940
1868
|
break;
|
|
1869
|
+
// The person's act (pod admin → Marketplace → Updates): nothing failed.
|
|
1870
|
+
case APPLY_IN_POD_ADMIN:
|
|
1871
|
+
toApplyInPodAdmin++;
|
|
1872
|
+
break;
|
|
1941
1873
|
default:
|
|
1942
1874
|
failed++;
|
|
1943
1875
|
}
|
|
@@ -1949,6 +1881,8 @@ export function summarizeApplyResults(results) {
|
|
|
1949
1881
|
// every surface — never "completed with failures" (review2 UX §1 #2).
|
|
1950
1882
|
if (withFailures > 0)
|
|
1951
1883
|
parts.push(`${withFailures} ${templateStepPhrase("unfinished")}`);
|
|
1884
|
+
if (toApplyInPodAdmin > 0)
|
|
1885
|
+
parts.push(`${toApplyInPodAdmin} to apply in pod admin`);
|
|
1952
1886
|
if (failed > 0)
|
|
1953
1887
|
parts.push(`${failed} failed`);
|
|
1954
1888
|
if (timedOut > 0)
|
|
@@ -1958,8 +1892,8 @@ export function summarizeApplyResults(results) {
|
|
|
1958
1892
|
const clean = withFailures === 0 && failed === 0 && timedOut === 0 && needsConsent === 0;
|
|
1959
1893
|
// Proposed isn't a failure (never ⚠) but isn't live yet (not a bare ✓) —
|
|
1960
1894
|
// a distinct "→ waiting for your approval" marker keeps the summary honest.
|
|
1961
|
-
const marker = !clean ? "⚠" : proposed > 0 ? "→" : "✓";
|
|
1962
|
-
const color = !clean ? chalk.yellow : proposed > 0 ? chalk.cyan : chalk.green;
|
|
1895
|
+
const marker = !clean ? "⚠" : proposed > 0 || toApplyInPodAdmin > 0 ? "→" : "✓";
|
|
1896
|
+
const color = !clean ? chalk.yellow : proposed > 0 || toApplyInPodAdmin > 0 ? chalk.cyan : chalk.green;
|
|
1963
1897
|
return {
|
|
1964
1898
|
updated,
|
|
1965
1899
|
unchanged,
|
|
@@ -1968,6 +1902,7 @@ export function summarizeApplyResults(results) {
|
|
|
1968
1902
|
timedOut,
|
|
1969
1903
|
proposed,
|
|
1970
1904
|
needsConsent,
|
|
1905
|
+
toApplyInPodAdmin,
|
|
1971
1906
|
line: color(`${marker} ${parts.join(" · ")}`),
|
|
1972
1907
|
};
|
|
1973
1908
|
}
|