@odla-ai/cli 0.36.1 → 0.37.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.
- package/dist/bin.cjs +77 -54
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-4QJ5NS64.js → chunk-XFIFFYNH.js} +70 -55
- package/dist/chunk-XFIFFYNH.js.map +1 -0
- package/dist/{cli-BFGY7F6X.js → cli-OH6PCYZ3.js} +2 -2
- package/dist/index.cjs +69 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-4QJ5NS64.js.map +0 -1
- /package/dist/{cli-BFGY7F6X.js.map → cli-OH6PCYZ3.js.map} +0 -0
package/dist/bin.cjs
CHANGED
|
@@ -838,6 +838,7 @@ var init_admin_ai_auth = __esm({
|
|
|
838
838
|
"app:config:write": "apply or inspect one revision-bound configuration operation for an app you own",
|
|
839
839
|
"platform:runbook:write": "read and edit all of odla's operational runbooks, including admin-visible content",
|
|
840
840
|
"app:device:enroll": "enrol this machine so it can mint its own short-lived credentials without asking you again",
|
|
841
|
+
"platform:device:enroll:extended": "enrol this machine with a credential that outlives the seven-day owner ceiling \u2014 a durable secret on a machine you are vouching for",
|
|
841
842
|
"platform:ai:policy:write": "change System AI model routing",
|
|
842
843
|
"platform:ai:policy:read": "read System AI model routing",
|
|
843
844
|
"platform:ai:credential:write": "replace a stored AI provider key",
|
|
@@ -7646,7 +7647,7 @@ function createCodeRuntimeControlClient(options) {
|
|
|
7646
7647
|
throw new TypeError("modelRequestTimeoutMs must be an integer from 30000 to 1800000");
|
|
7647
7648
|
}
|
|
7648
7649
|
const request3 = options.fetch ?? fetch;
|
|
7649
|
-
const
|
|
7650
|
+
const call3 = async (path, body, timeoutMs = requestTimeoutMs) => {
|
|
7650
7651
|
const timeout = AbortSignal.timeout(timeoutMs);
|
|
7651
7652
|
const signal = options.signal ? AbortSignal.any([options.signal, timeout]) : timeout;
|
|
7652
7653
|
let response2;
|
|
@@ -7676,17 +7677,17 @@ function createCodeRuntimeControlClient(options) {
|
|
|
7676
7677
|
return {
|
|
7677
7678
|
heartbeat: async (version, capabilities) => {
|
|
7678
7679
|
validateHeartbeat(version, capabilities);
|
|
7679
|
-
return parseSnapshot(await
|
|
7680
|
+
return parseSnapshot(await call3("/registry/code/runtime/heartbeat", { runtimeVersion: version, capabilities }));
|
|
7680
7681
|
},
|
|
7681
7682
|
acknowledge: async (commandId, result) => {
|
|
7682
7683
|
if (!/^ccmd_[0-9a-f]{32}$/.test(commandId)) throw new TypeError("invalid Code runtime command id");
|
|
7683
|
-
await
|
|
7684
|
+
await call3(`/registry/code/runtime/commands/${commandId}/ack`, result);
|
|
7684
7685
|
},
|
|
7685
7686
|
source: async (sessionId) => parseSource(
|
|
7686
|
-
await
|
|
7687
|
+
await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/source`, {})
|
|
7687
7688
|
),
|
|
7688
7689
|
infer: async (sessionId, inference) => {
|
|
7689
|
-
const value2 = record5(await
|
|
7690
|
+
const value2 = record5(await call3(
|
|
7690
7691
|
`/registry/code/runtime/sessions/${validSessionId(sessionId)}/inference`,
|
|
7691
7692
|
inference,
|
|
7692
7693
|
modelRequestTimeoutMs
|
|
@@ -7697,11 +7698,11 @@ function createCodeRuntimeControlClient(options) {
|
|
|
7697
7698
|
return value2;
|
|
7698
7699
|
},
|
|
7699
7700
|
review: async (sessionId, review) => parseReview(
|
|
7700
|
-
await
|
|
7701
|
+
await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/review`, review, modelRequestTimeoutMs)
|
|
7701
7702
|
),
|
|
7702
7703
|
submitCandidate: async (sessionId, checkpointId, verification) => {
|
|
7703
7704
|
if (!/^cpoint_[0-9a-f]{32}$/.test(checkpointId)) throw new TypeError("invalid Code checkpoint id");
|
|
7704
|
-
return parseCandidate(await
|
|
7705
|
+
return parseCandidate(await call3(
|
|
7705
7706
|
`/registry/code/runtime/sessions/${validSessionId(sessionId)}/candidates`,
|
|
7706
7707
|
{ checkpointId, verification }
|
|
7707
7708
|
));
|
|
@@ -7711,21 +7712,21 @@ function createCodeRuntimeControlClient(options) {
|
|
|
7711
7712
|
if (!/^[A-Za-z0-9._:-]{1,120}$/.test(eventId) || !event || typeof event !== "object" || new TextEncoder().encode(serialized).byteLength > 24e3) {
|
|
7712
7713
|
throw new TypeError("invalid Code session event");
|
|
7713
7714
|
}
|
|
7714
|
-
await
|
|
7715
|
+
await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/chat/events`, { eventId, event });
|
|
7715
7716
|
},
|
|
7716
7717
|
recallMemories: async (sessionId, subjects, limit) => {
|
|
7717
|
-
const response2 = await
|
|
7718
|
+
const response2 = await call3(
|
|
7718
7719
|
`/registry/code/runtime/sessions/${validSessionId(sessionId)}/recall`,
|
|
7719
7720
|
{ subjects: [...subjects], limit }
|
|
7720
7721
|
);
|
|
7721
7722
|
return Array.isArray(response2.memories) ? response2.memories : [];
|
|
7722
7723
|
},
|
|
7723
7724
|
rememberMemory: async (sessionId, memory) => {
|
|
7724
|
-
await
|
|
7725
|
+
await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/remember`, memory);
|
|
7725
7726
|
},
|
|
7726
7727
|
reportSessionFailure: async (sessionId, message2) => {
|
|
7727
7728
|
if (!message2.trim() || message2.length > 2e3) throw new TypeError("invalid Code session failure");
|
|
7728
|
-
await
|
|
7729
|
+
await call3(`/registry/code/runtime/sessions/${validSessionId(sessionId)}/failure`, { message: message2 });
|
|
7729
7730
|
}
|
|
7730
7731
|
};
|
|
7731
7732
|
}
|
|
@@ -8511,7 +8512,7 @@ async function materializeCommandWorkspace(input) {
|
|
|
8511
8512
|
}
|
|
8512
8513
|
function codeSkill(opts) {
|
|
8513
8514
|
let seq = 0;
|
|
8514
|
-
const
|
|
8515
|
+
const call3 = async (tool, input, signal) => {
|
|
8515
8516
|
const startedAt = Date.now();
|
|
8516
8517
|
const response2 = await opts.broker.execute(
|
|
8517
8518
|
{ lease: opts.lease, workspaceDir: opts.workspaceDir, signal },
|
|
@@ -8533,7 +8534,7 @@ function codeSkill(opts) {
|
|
|
8533
8534
|
},
|
|
8534
8535
|
additionalProperties: false
|
|
8535
8536
|
},
|
|
8536
|
-
handler: (input, ctx) =>
|
|
8537
|
+
handler: (input, ctx) => call3("sandbox.read", input, ctx.signal)
|
|
8537
8538
|
};
|
|
8538
8539
|
const applyPatch = {
|
|
8539
8540
|
name: "odla_apply_git_diff",
|
|
@@ -8544,7 +8545,7 @@ function codeSkill(opts) {
|
|
|
8544
8545
|
properties: { patch: { type: "string", minLength: 1, maxLength: 262144 } },
|
|
8545
8546
|
additionalProperties: false
|
|
8546
8547
|
},
|
|
8547
|
-
handler: (input, ctx) =>
|
|
8548
|
+
handler: (input, ctx) => call3("sandbox.apply_patch", input, ctx.signal)
|
|
8548
8549
|
};
|
|
8549
8550
|
const runRecipe = {
|
|
8550
8551
|
name: "odla_run_recipe",
|
|
@@ -8555,7 +8556,7 @@ function codeSkill(opts) {
|
|
|
8555
8556
|
properties: { recipeId: { type: "string", minLength: 1, maxLength: 120, pattern: "^[a-zA-Z0-9._:-]+$" } },
|
|
8556
8557
|
additionalProperties: false
|
|
8557
8558
|
},
|
|
8558
|
-
handler: (input, ctx) =>
|
|
8559
|
+
handler: (input, ctx) => call3("sandbox.run_recipe", input, ctx.signal)
|
|
8559
8560
|
};
|
|
8560
8561
|
const listFiles2 = {
|
|
8561
8562
|
name: "odla_list",
|
|
@@ -8568,7 +8569,7 @@ function codeSkill(opts) {
|
|
|
8568
8569
|
},
|
|
8569
8570
|
additionalProperties: false
|
|
8570
8571
|
},
|
|
8571
|
-
handler: (input, ctx) =>
|
|
8572
|
+
handler: (input, ctx) => call3("sandbox.list", input, ctx.signal)
|
|
8572
8573
|
};
|
|
8573
8574
|
const searchFiles = {
|
|
8574
8575
|
name: "odla_search",
|
|
@@ -8584,7 +8585,7 @@ function codeSkill(opts) {
|
|
|
8584
8585
|
},
|
|
8585
8586
|
additionalProperties: false
|
|
8586
8587
|
},
|
|
8587
|
-
handler: (input, ctx) =>
|
|
8588
|
+
handler: (input, ctx) => call3("sandbox.search", input, ctx.signal)
|
|
8588
8589
|
};
|
|
8589
8590
|
const graphTool = (name, tool, description, required) => ({
|
|
8590
8591
|
name,
|
|
@@ -8595,7 +8596,7 @@ function codeSkill(opts) {
|
|
|
8595
8596
|
properties: { query: { type: "string", maxLength: 512 } },
|
|
8596
8597
|
additionalProperties: false
|
|
8597
8598
|
},
|
|
8598
|
-
handler: (input, ctx) =>
|
|
8599
|
+
handler: (input, ctx) => call3(tool, input, ctx.signal)
|
|
8599
8600
|
});
|
|
8600
8601
|
const orientation = [
|
|
8601
8602
|
graphTool(
|
|
@@ -8634,9 +8635,9 @@ async function runCodeAgent(options) {
|
|
|
8634
8635
|
lease: options.lease,
|
|
8635
8636
|
workspaceDir: options.workspaceDir,
|
|
8636
8637
|
surface,
|
|
8637
|
-
onToolCall: (
|
|
8638
|
-
toolCalls.push(
|
|
8639
|
-
options.onToolCall?.(
|
|
8638
|
+
onToolCall: (call3) => {
|
|
8639
|
+
toolCalls.push(call3);
|
|
8640
|
+
options.onToolCall?.(call3);
|
|
8640
8641
|
}
|
|
8641
8642
|
});
|
|
8642
8643
|
const compaction = options.compaction === void 0 ? (0, import_ai4.keepRecentExchanges)({ whenInputTokensExceed: 12e4, keep: 3 }) : options.compaction;
|
|
@@ -11184,7 +11185,7 @@ Usage:
|
|
|
11184
11185
|
odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
|
|
11185
11186
|
odla-ai security run [target] --self --ack-redacted-source
|
|
11186
11187
|
odla-ai provision [--live] [--config odla.config.mjs] [--email <odla-account>] [--request-grant] [--no-open] [--wait <seconds>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
|
|
11187
|
-
odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--email <odla-account>] [--no-open] [--json]
|
|
11188
|
+
odla-ai device enroll [--app <id>[,<id>...]] [--name <label>] [--capability <c>[,<c>...]] [--device-ttl <30d|6w|2y|forever>] [--email <odla-account>] [--no-open] [--json]
|
|
11188
11189
|
odla-ai device list [--email <odla-account>] [--json]
|
|
11189
11190
|
odla-ai device revoke <device-id> [--email <odla-account>] [--json]
|
|
11190
11191
|
odla-ai credentials list [--config odla.config.mjs] [--env dev] [--all] [--json]
|
|
@@ -13865,9 +13866,15 @@ async function provisionEnvCredentials(opts) {
|
|
|
13865
13866
|
if (o11yToken) {
|
|
13866
13867
|
opts.stdout.log(`${opts.env}: reusing local o11y ingest token`);
|
|
13867
13868
|
} else {
|
|
13868
|
-
o11yToken = await issueO11yToken(opts);
|
|
13869
|
-
|
|
13870
|
-
|
|
13869
|
+
o11yToken = await issueO11yToken(opts) ?? void 0;
|
|
13870
|
+
if (o11yToken) {
|
|
13871
|
+
credentials = save(opts, credentials, tenantId, { ...dbKey ? { dbKey } : {}, o11yToken });
|
|
13872
|
+
opts.stdout.log(`${opts.env}: ${opts.rotateO11y ? "rotated" : "issued"} o11y ingest token`);
|
|
13873
|
+
} else {
|
|
13874
|
+
opts.stdout.log(
|
|
13875
|
+
`${opts.env}: o11y token already exists and its shown-once value is not on this machine \u2014 leaving it alone (a co-owner provisioned this env). Nothing was rotated, and this env's running Worker keeps its token. To replace it deliberately \u2014 which invalidates the token on whatever is deployed \u2014 run "odla-ai provision --rotate-o11y-token --push-secrets".`
|
|
13876
|
+
);
|
|
13877
|
+
}
|
|
13871
13878
|
}
|
|
13872
13879
|
}
|
|
13873
13880
|
return save(opts, credentials, tenantId, {
|
|
@@ -13921,11 +13928,7 @@ async function issueO11yToken(opts) {
|
|
|
13921
13928
|
`${opts.cfg.platformUrl}/o11y/${encodeURIComponent(opts.cfg.app.id)}/token${suffix}?env=${encodeURIComponent(opts.env)}`,
|
|
13922
13929
|
{ method: "POST", headers: { authorization: `Bearer ${opts.developerToken}` } }
|
|
13923
13930
|
);
|
|
13924
|
-
if (res.status === 409 && !opts.rotateO11y)
|
|
13925
|
-
throw new Error(
|
|
13926
|
-
`o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --push-secrets" to install a separate runtime credential without rotating siblings`
|
|
13927
|
-
);
|
|
13928
|
-
}
|
|
13931
|
+
if (res.status === 409 && !opts.rotateO11y) return null;
|
|
13929
13932
|
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText7(res)}`);
|
|
13930
13933
|
const body = await res.json();
|
|
13931
13934
|
if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
|
|
@@ -14546,6 +14549,31 @@ var init_advisory_output = __esm({
|
|
|
14546
14549
|
}
|
|
14547
14550
|
});
|
|
14548
14551
|
|
|
14552
|
+
// src/device-ttl.ts
|
|
14553
|
+
function parseDeviceTtl(raw) {
|
|
14554
|
+
if (raw === void 0 || raw === true) return void 0;
|
|
14555
|
+
const text3 = String(raw).trim().toLowerCase();
|
|
14556
|
+
if (!text3) return void 0;
|
|
14557
|
+
if (text3 === "forever" || text3 === "never") return 100 * 365 * DAY_MS;
|
|
14558
|
+
const match = /^(\d+)\s*([dwy])$/.exec(text3);
|
|
14559
|
+
if (!match) {
|
|
14560
|
+
throw new Error(
|
|
14561
|
+
`--device-ttl expects a duration like 30d, 6w, 2y, or "forever" (got "${raw}")`
|
|
14562
|
+
);
|
|
14563
|
+
}
|
|
14564
|
+
return Number(match[1]) * UNITS[match[2]];
|
|
14565
|
+
}
|
|
14566
|
+
var OWNER_DEVICE_TTL_MS, DAY_MS, UNITS;
|
|
14567
|
+
var init_device_ttl = __esm({
|
|
14568
|
+
"src/device-ttl.ts"() {
|
|
14569
|
+
"use strict";
|
|
14570
|
+
init_cjs_shims();
|
|
14571
|
+
OWNER_DEVICE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
14572
|
+
DAY_MS = 24 * 60 * 60 * 1e3;
|
|
14573
|
+
UNITS = { d: DAY_MS, w: 7 * DAY_MS, y: 365 * DAY_MS };
|
|
14574
|
+
}
|
|
14575
|
+
});
|
|
14576
|
+
|
|
14549
14577
|
// src/device-command.ts
|
|
14550
14578
|
async function deviceCommand(parsed, deps) {
|
|
14551
14579
|
const action2 = parsed.positionals[1] ?? "";
|
|
@@ -14562,7 +14590,17 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
|
|
|
14562
14590
|
const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
|
|
14563
14591
|
const apps = (stringOpt(parsed.options.app) ?? cfg.app.id).split(",").map((id2) => id2.trim()).filter(Boolean);
|
|
14564
14592
|
if (apps.length === 0) throw new Error("device enroll needs --app <id>[,<id>\u2026]");
|
|
14565
|
-
const
|
|
14593
|
+
const deviceTtlMs = parseDeviceTtl(parsed.options["device-ttl"]);
|
|
14594
|
+
const extended = deviceTtlMs !== void 0 && deviceTtlMs > OWNER_DEVICE_TTL_MS;
|
|
14595
|
+
const token = await scopedToken2(
|
|
14596
|
+
parsed,
|
|
14597
|
+
deps,
|
|
14598
|
+
cfg,
|
|
14599
|
+
doFetch,
|
|
14600
|
+
out,
|
|
14601
|
+
`odla CLI (enroll ${name})`,
|
|
14602
|
+
extended ? "platform:device:enroll:extended" : "app:device:enroll"
|
|
14603
|
+
);
|
|
14566
14604
|
const response2 = await doFetch(`${cfg.platformUrl}/registry/devices`, {
|
|
14567
14605
|
method: "POST",
|
|
14568
14606
|
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
@@ -14570,7 +14608,8 @@ async function enroll(parsed, deps, cfg, doFetch, out, json) {
|
|
|
14570
14608
|
name,
|
|
14571
14609
|
platform: import_node_process15.default.platform,
|
|
14572
14610
|
appIds: apps,
|
|
14573
|
-
...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {}
|
|
14611
|
+
...parsed.options.capability ? { capabilities: String(parsed.options.capability).split(",").map((c) => c.trim()).filter(Boolean) } : {},
|
|
14612
|
+
...deviceTtlMs === void 0 ? {} : { deviceTtlMs }
|
|
14574
14613
|
})
|
|
14575
14614
|
});
|
|
14576
14615
|
const body = await response2.json().catch(() => ({}));
|
|
@@ -14623,12 +14662,12 @@ async function revoke(parsed, deps, cfg, doFetch, out, json) {
|
|
|
14623
14662
|
out.error(`device: revoked ${deviceId}; every credential it minted is revoked with it`);
|
|
14624
14663
|
if (json) out.log(JSON.stringify({ deviceId, revoked: true }, null, 2));
|
|
14625
14664
|
}
|
|
14626
|
-
async function scopedToken2(parsed, deps, cfg, doFetch, out, label) {
|
|
14665
|
+
async function scopedToken2(parsed, deps, cfg, doFetch, out, label, scope = "app:device:enroll") {
|
|
14627
14666
|
const { credentials } = await resolveOperatorContext(parsed, { allowMissingConfig: true });
|
|
14628
14667
|
const scopedTokenFile = credentials.scopedTokenFile;
|
|
14629
14668
|
return getScopedPlatformToken({
|
|
14630
14669
|
platform: cfg.platformUrl,
|
|
14631
|
-
scope
|
|
14670
|
+
scope,
|
|
14632
14671
|
email: stringOpt(parsed.options.email),
|
|
14633
14672
|
label,
|
|
14634
14673
|
fetch: doFetch,
|
|
@@ -14648,6 +14687,7 @@ var init_device_command = __esm({
|
|
|
14648
14687
|
"src/device-command.ts"() {
|
|
14649
14688
|
"use strict";
|
|
14650
14689
|
init_cjs_shims();
|
|
14690
|
+
init_device_ttl();
|
|
14651
14691
|
import_node_fs20 = require("fs");
|
|
14652
14692
|
import_node_path18 = require("path");
|
|
14653
14693
|
import_node_process15 = __toESM(require("process"), 1);
|
|
@@ -14860,12 +14900,7 @@ ${counts.created} created, ${counts.updated} updated, ${counts.unchanged} unchan
|
|
|
14860
14900
|
);
|
|
14861
14901
|
}
|
|
14862
14902
|
async function upsert(ctx, r, visibility) {
|
|
14863
|
-
const
|
|
14864
|
-
ctx,
|
|
14865
|
-
"GET",
|
|
14866
|
-
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(r.slug)}&limit=1`
|
|
14867
|
-
);
|
|
14868
|
-
const found = page2.records[0];
|
|
14903
|
+
const found = await bySlug(ctx, r.slug).catch(() => null);
|
|
14869
14904
|
if (!found) {
|
|
14870
14905
|
await call(ctx, "POST", "/runbook", {
|
|
14871
14906
|
appId: ctx.appId,
|
|
@@ -15303,13 +15338,7 @@ async function runbookAsk(ctx, question, all) {
|
|
|
15303
15338
|
ctx.out.log(JSDOC_POINTER);
|
|
15304
15339
|
}
|
|
15305
15340
|
async function runbookComment(ctx, slug, body) {
|
|
15306
|
-
const
|
|
15307
|
-
ctx,
|
|
15308
|
-
"GET",
|
|
15309
|
-
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
|
|
15310
|
-
);
|
|
15311
|
-
const found = page2.records[0];
|
|
15312
|
-
if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
|
|
15341
|
+
const found = await bySlug(ctx, slug);
|
|
15313
15342
|
await call(ctx, "POST", `/runbook/${encodeURIComponent(found.id)}/comments`, { body });
|
|
15314
15343
|
ctx.out.log(`commented on ${slug} (v${found.version})`);
|
|
15315
15344
|
}
|
|
@@ -15376,13 +15405,7 @@ var init_runbook_editor = __esm({
|
|
|
15376
15405
|
|
|
15377
15406
|
// src/runbook-edit-flow.ts
|
|
15378
15407
|
async function editRunbook(ctx, slug, deps = {}) {
|
|
15379
|
-
const
|
|
15380
|
-
ctx,
|
|
15381
|
-
"GET",
|
|
15382
|
-
`/runbook?app=${encodeURIComponent(ctx.appId)}&slug=${encodeURIComponent(slug)}&limit=1`
|
|
15383
|
-
);
|
|
15384
|
-
const found = page2.records[0];
|
|
15385
|
-
if (!found) throw new Error(`no runbook "${slug}" in ${ctx.appId}`);
|
|
15408
|
+
const found = await bySlug(ctx, slug);
|
|
15386
15409
|
ctx.out.log(`opening ${slug} v${found.version} in your editor\u2026`);
|
|
15387
15410
|
const body = await editText(found.body, slug, deps);
|
|
15388
15411
|
return body === null ? null : { body, expectedVersion: found.version };
|