@odla-ai/cli 0.16.7 → 0.17.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 +346 -61
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-X3HUN6S3.js → chunk-2UKO2NED.js} +312 -20
- package/dist/chunk-2UKO2NED.js.map +1 -0
- package/dist/index.cjs +346 -61
- 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 +4 -4
- package/skills/odla/SKILL.md +7 -5
- package/skills/odla/references/sdks.md +9 -0
- package/skills/odla-migrate/SKILL.md +4 -2
- package/skills/odla-migrate/references/phase-2-chapter.md +90 -0
- package/skills/odla-migrate/references/phase-2-db.md +10 -1
- package/dist/chunk-X3HUN6S3.js.map +0 -1
package/dist/bin.cjs
CHANGED
|
@@ -1239,14 +1239,36 @@ function unique2(values) {
|
|
|
1239
1239
|
return [...new Set(values.filter(Boolean))];
|
|
1240
1240
|
}
|
|
1241
1241
|
|
|
1242
|
+
// src/tenant.ts
|
|
1243
|
+
var import_apps = require("@odla-ai/apps");
|
|
1244
|
+
function resolveEnv(cfg, requested) {
|
|
1245
|
+
const env = requested ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
|
|
1246
|
+
if (!env || !cfg.envs.includes(env)) {
|
|
1247
|
+
throw new Error(`env "${env ?? ""}" is not declared in ${cfg.configPath}`);
|
|
1248
|
+
}
|
|
1249
|
+
return env;
|
|
1250
|
+
}
|
|
1251
|
+
function resolveTenant(cfg, requested) {
|
|
1252
|
+
const env = resolveEnv(cfg, requested);
|
|
1253
|
+
return { env, tenant: (0, import_apps.tenantIdFor)(cfg.app.id, env) };
|
|
1254
|
+
}
|
|
1255
|
+
function bothTenants(cfg) {
|
|
1256
|
+
for (const env of ["dev", "prod"]) {
|
|
1257
|
+
if (!cfg.envs.includes(env)) {
|
|
1258
|
+
throw new Error(
|
|
1259
|
+
`env "${env}" is not declared in ${cfg.configPath} \u2014 add it to \`envs\` and run \`odla-ai provision --yes\` before transferring data between environments`
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return { sandbox: (0, import_apps.tenantIdFor)(cfg.app.id, "dev"), live: (0, import_apps.tenantIdFor)(cfg.app.id, "prod") };
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1242
1266
|
// src/app-export.ts
|
|
1243
1267
|
async function appExport(options) {
|
|
1244
1268
|
const cfg = await loadProjectConfig(options.configPath);
|
|
1245
1269
|
const out = options.stdout ?? console;
|
|
1246
1270
|
const doFetch = options.fetch ?? fetch;
|
|
1247
|
-
const
|
|
1248
|
-
if (!env || !cfg.envs.includes(env)) throw new Error(`env "${env ?? ""}" is not declared in ${options.configPath}`);
|
|
1249
|
-
const tenant = env === "prod" ? cfg.app.id : `${cfg.app.id}--${env}`;
|
|
1271
|
+
const { tenant } = resolveTenant(cfg, options.env);
|
|
1250
1272
|
const token = await getDeveloperToken(
|
|
1251
1273
|
cfg,
|
|
1252
1274
|
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
@@ -1281,6 +1303,84 @@ async function appExport(options) {
|
|
|
1281
1303
|
return { file, backup: newest };
|
|
1282
1304
|
}
|
|
1283
1305
|
|
|
1306
|
+
// src/app-import.ts
|
|
1307
|
+
var import_node_fs6 = require("fs");
|
|
1308
|
+
var import_import = require("@odla-ai/db/import");
|
|
1309
|
+
function chooseIdMode(options, rows) {
|
|
1310
|
+
const chosen = [options.idField && "field", options.key && "key", options.generateIds && "generate"].filter(Boolean);
|
|
1311
|
+
if (chosen.length > 1) throw new Error("choose one of --id-field, --key, or --generate-ids");
|
|
1312
|
+
if (options.key) return { idMode: "key" };
|
|
1313
|
+
if (options.generateIds) return { idMode: "generate" };
|
|
1314
|
+
if (options.idField) return { idMode: "field", idField: options.idField };
|
|
1315
|
+
if (rows.length > 0 && rows.every((row) => typeof row?.id === "string" && row.id)) return { idMode: "field" };
|
|
1316
|
+
throw new Error(
|
|
1317
|
+
"rows have no usable `id` \u2014 pass --id-field <field> to name the id column, --key <unique-attr> to match on a unique attribute, or --generate-ids to mint new ids (which is NOT idempotent: re-running duplicates rows)"
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
async function appImport(options) {
|
|
1321
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
1322
|
+
const out = options.stdout ?? console;
|
|
1323
|
+
const doFetch = options.fetch ?? fetch;
|
|
1324
|
+
const { tenant } = resolveTenant(cfg, options.env);
|
|
1325
|
+
const text = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs6.readFileSync)(0, "utf8")))() : (0, import_node_fs6.readFileSync)(options.file, "utf8");
|
|
1326
|
+
const { format, sources } = (0, import_import.parseImport)(text, options.ns);
|
|
1327
|
+
if (format === "namespace-map" && options.ns) {
|
|
1328
|
+
throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
|
|
1329
|
+
}
|
|
1330
|
+
if (format !== "namespace-map" && !options.ns) throw new Error("--ns is required unless the file is a {namespace: rows} map");
|
|
1331
|
+
if (sources.length === 0) throw new Error(`${options.file} contains no rows`);
|
|
1332
|
+
const mode = chooseIdMode(options, sources.map((s) => s.row));
|
|
1333
|
+
const build = { ...mode, keyAttr: options.key };
|
|
1334
|
+
const { ops, rejected, ignored } = (0, import_import.buildImportOps)(sources, build);
|
|
1335
|
+
if (rejected.length > 0) {
|
|
1336
|
+
const detail = rejected.slice(0, 10).map((r) => ` row ${r.index}: ${r.reason}`).join("\n");
|
|
1337
|
+
const more = rejected.length > 10 ? `
|
|
1338
|
+
\u2026and ${rejected.length - 10} more` : "";
|
|
1339
|
+
throw new Error(`${rejected.length} row(s) cannot be imported \u2014 fix the input and re-run:
|
|
1340
|
+
${detail}${more}`);
|
|
1341
|
+
}
|
|
1342
|
+
const chunks = (0, import_import.planImportChunks)(ops);
|
|
1343
|
+
const result = { tenant, rows: ops.length, chunks: chunks.length, committed: 0, duplicate: 0, txIds: [], ignored };
|
|
1344
|
+
if (ignored.length > 0) out.log(`ignoring server-maintained field(s): ${ignored.join(", ")} \u2014 odla-db stamps these on write`);
|
|
1345
|
+
if (options.dryRun || options.yes !== true) {
|
|
1346
|
+
const why = options.dryRun ? "--dry-run" : "no --yes";
|
|
1347
|
+
out.log(`${tenant}: would upsert ${ops.length} row(s) across ${chunks.length} transaction(s) \u2014 nothing written (${why})`);
|
|
1348
|
+
if (options.json) out.log(JSON.stringify(result, null, 2));
|
|
1349
|
+
return result;
|
|
1350
|
+
}
|
|
1351
|
+
const token = await getDeveloperToken(
|
|
1352
|
+
cfg,
|
|
1353
|
+
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1354
|
+
doFetch,
|
|
1355
|
+
out
|
|
1356
|
+
);
|
|
1357
|
+
const runId = crypto.randomUUID();
|
|
1358
|
+
const url = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}/transact`;
|
|
1359
|
+
for (const chunk of chunks) {
|
|
1360
|
+
const res = await doFetch(url, {
|
|
1361
|
+
method: "POST",
|
|
1362
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1363
|
+
body: JSON.stringify({ mutationId: (0, import_import.importMutationId)(runId, chunk.index), ops: chunk.ops })
|
|
1364
|
+
});
|
|
1365
|
+
const body = await res.json().catch(() => ({}));
|
|
1366
|
+
if (!res.ok) {
|
|
1367
|
+
const code = body.error?.code ? ` (${body.error.code})` : "";
|
|
1368
|
+
throw new Error(
|
|
1369
|
+
`chunk ${chunk.index + 1} of ${chunks.length} failed${code}: ${body.error?.message ?? res.status}. ${result.committed} row(s) already committed; fix the input and re-run to import the rest.`
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
result.committed += chunk.ops.length;
|
|
1373
|
+
if (typeof body.txId === "number") result.txIds.push(body.txId);
|
|
1374
|
+
if (body.duplicate) result.duplicate++;
|
|
1375
|
+
}
|
|
1376
|
+
if (options.json) out.log(JSON.stringify(result, null, 2));
|
|
1377
|
+
else {
|
|
1378
|
+
const dup = result.duplicate > 0 ? ` (${result.duplicate} chunk(s) were already applied)` : "";
|
|
1379
|
+
out.log(`${tenant}: upserted ${result.committed} row(s) in ${chunks.length} transaction(s)${dup}`);
|
|
1380
|
+
}
|
|
1381
|
+
return result;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1284
1384
|
// src/app-owners.ts
|
|
1285
1385
|
var sink = (options) => options.stdout ?? console;
|
|
1286
1386
|
async function ownersRequest(method, suffix, options, body) {
|
|
@@ -1368,6 +1468,133 @@ async function appOwnersCommand(parsed, dependencies = {}) {
|
|
|
1368
1468
|
);
|
|
1369
1469
|
}
|
|
1370
1470
|
|
|
1471
|
+
// src/app-transfer.ts
|
|
1472
|
+
function endpointsFor(verb, tenants) {
|
|
1473
|
+
const up = { source: tenants.sandbox, target: tenants.live, from: "dev" };
|
|
1474
|
+
const down = { source: tenants.live, target: tenants.sandbox, from: "prod" };
|
|
1475
|
+
if (verb === "refresh-sandbox") return { ...down, mode: "refresh" };
|
|
1476
|
+
return { ...up, mode: "cutover" };
|
|
1477
|
+
}
|
|
1478
|
+
async function api(cfg, token, path, init, doFetch) {
|
|
1479
|
+
const res = await doFetch(`${cfg.dbEndpoint}${path}`, {
|
|
1480
|
+
...init,
|
|
1481
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...init?.headers }
|
|
1482
|
+
});
|
|
1483
|
+
return { status: res.status, body: await res.json().catch(() => ({})) };
|
|
1484
|
+
}
|
|
1485
|
+
function describe(side) {
|
|
1486
|
+
if (!side.exists) return "not provisioned";
|
|
1487
|
+
if (side.maxTx === 0) return "empty \u2014 never written";
|
|
1488
|
+
const ns = side.namespaces.length === 1 ? "1 namespace" : `${side.namespaces.length} namespaces`;
|
|
1489
|
+
const identity = side.identityRows > 0 ? `, ${side.identityRows} identity row(s)` : "";
|
|
1490
|
+
return `${ns} \xB7 ${side.triples} value(s) \xB7 tx ${side.maxTx}${identity}`;
|
|
1491
|
+
}
|
|
1492
|
+
function printPlan(out, verb, pre, opts) {
|
|
1493
|
+
const arrow = verb === "refresh-sandbox" ? "live \u2192 sandbox" : "sandbox \u2192 live";
|
|
1494
|
+
out.log(`${verb} (${arrow})`);
|
|
1495
|
+
out.log(` from ${pre.source.tenant} ${describe(pre.source)}`);
|
|
1496
|
+
out.log(` to ${pre.target.tenant} ${describe(pre.target)}`);
|
|
1497
|
+
if (verb === "promote") {
|
|
1498
|
+
out.log(" moves schema, rules and gates only \u2014 no rows are copied or removed");
|
|
1499
|
+
} else {
|
|
1500
|
+
out.log(` ${pre.target.tenant} is REPLACED by ${pre.source.tenant}`);
|
|
1501
|
+
out.log(` stays put: ${pre.staysPut.join(", ")}`);
|
|
1502
|
+
const identity = opts.includeIdentity ? "INCLUDED (--include-identity)" : `left behind: ${pre.excluded.join(", ")}`;
|
|
1503
|
+
out.log(` identity: ${identity}`);
|
|
1504
|
+
out.log(` files: ${opts.includeFiles ? "copied (--include-files)" : "not copied"}`);
|
|
1505
|
+
}
|
|
1506
|
+
for (const blocker of pre.blockers) out.log(` \u2716 ${blocker.code}: ${blocker.message}`);
|
|
1507
|
+
}
|
|
1508
|
+
async function appTransfer(options) {
|
|
1509
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
1510
|
+
const out = options.stdout ?? console;
|
|
1511
|
+
const doFetch = options.fetch ?? fetch;
|
|
1512
|
+
const tenants = bothTenants(cfg);
|
|
1513
|
+
const route2 = endpointsFor(options.verb, tenants);
|
|
1514
|
+
const token = await getDeveloperToken(
|
|
1515
|
+
cfg,
|
|
1516
|
+
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1517
|
+
doFetch,
|
|
1518
|
+
out
|
|
1519
|
+
);
|
|
1520
|
+
const pre = await api(
|
|
1521
|
+
cfg,
|
|
1522
|
+
token,
|
|
1523
|
+
`/admin/apps/${encodeURIComponent(route2.target)}/copy-preflight?from=${route2.from}`,
|
|
1524
|
+
void 0,
|
|
1525
|
+
doFetch
|
|
1526
|
+
);
|
|
1527
|
+
if (pre.status !== 200) {
|
|
1528
|
+
throw new Error(`pre-flight failed${pre.body.error?.code ? ` (${pre.body.error.code})` : ""}: ${pre.body.error?.message ?? pre.status}`);
|
|
1529
|
+
}
|
|
1530
|
+
const plan = pre.body;
|
|
1531
|
+
printPlan(out, options.verb, plan, options);
|
|
1532
|
+
if (options.verb === "go-live" && !plan.targetEmpty) {
|
|
1533
|
+
throw new Error(
|
|
1534
|
+
`${route2.target} already has data \u2014 go-live has already happened. Use \`odla-ai app promote --yes\` to push schema and rules, or \`odla-ai app refresh-sandbox --yes\` to pull live back down.`
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
if (plan.blockers.length > 0) throw new Error(`cannot ${options.verb}: ${plan.blockers.map((b) => b.message).join("; ")}`);
|
|
1538
|
+
if (options.dryRun || options.yes !== true) {
|
|
1539
|
+
out.log(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
|
|
1540
|
+
if (options.json) out.log(JSON.stringify(plan, null, 2));
|
|
1541
|
+
return { ok: true, plan };
|
|
1542
|
+
}
|
|
1543
|
+
if (options.verb === "promote") {
|
|
1544
|
+
const res2 = await api(
|
|
1545
|
+
cfg,
|
|
1546
|
+
token,
|
|
1547
|
+
`/admin/apps/${encodeURIComponent(route2.target)}/promote-definitions`,
|
|
1548
|
+
{ method: "POST", body: JSON.stringify({ from: "dev" }) },
|
|
1549
|
+
doFetch
|
|
1550
|
+
);
|
|
1551
|
+
if (res2.status !== 200) throw new Error(`promote failed${res2.body.error?.code ? ` (${res2.body.error.code})` : ""}: ${res2.body.error?.message ?? res2.status}`);
|
|
1552
|
+
if (options.json) out.log(JSON.stringify(res2.body, null, 2));
|
|
1553
|
+
else out.log(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
|
|
1554
|
+
return { ok: true, plan, result: res2.body };
|
|
1555
|
+
}
|
|
1556
|
+
const res = await api(
|
|
1557
|
+
cfg,
|
|
1558
|
+
token,
|
|
1559
|
+
`/admin/apps/${encodeURIComponent(route2.target)}/copy-db`,
|
|
1560
|
+
{
|
|
1561
|
+
method: "POST",
|
|
1562
|
+
body: JSON.stringify({
|
|
1563
|
+
from: route2.from,
|
|
1564
|
+
mode: route2.mode,
|
|
1565
|
+
...options.includeIdentity ? { includeUsers: true } : {},
|
|
1566
|
+
...options.includeFiles ? { includeFiles: true } : {}
|
|
1567
|
+
})
|
|
1568
|
+
},
|
|
1569
|
+
doFetch
|
|
1570
|
+
);
|
|
1571
|
+
if (res.status !== 200) throw new Error(`${options.verb} failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
|
|
1572
|
+
out.log(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
|
|
1573
|
+
if (options.includeFiles) await copyFiles(cfg, token, route2, doFetch, out);
|
|
1574
|
+
if (options.json) out.log(JSON.stringify(res.body, null, 2));
|
|
1575
|
+
return { ok: true, plan, result: res.body };
|
|
1576
|
+
}
|
|
1577
|
+
async function copyFiles(cfg, token, route2, doFetch, out) {
|
|
1578
|
+
for (let attempt = 1; attempt <= 20; attempt++) {
|
|
1579
|
+
const res = await api(
|
|
1580
|
+
cfg,
|
|
1581
|
+
token,
|
|
1582
|
+
`/admin/apps/${encodeURIComponent(route2.target)}/copy-files`,
|
|
1583
|
+
{ method: "POST", body: JSON.stringify({ from: route2.from }) },
|
|
1584
|
+
doFetch
|
|
1585
|
+
);
|
|
1586
|
+
if (res.status === 200) {
|
|
1587
|
+
out.log(`${route2.target}: files copied`);
|
|
1588
|
+
return;
|
|
1589
|
+
}
|
|
1590
|
+
if (!res.body.error?.retry) {
|
|
1591
|
+
throw new Error(`file copy failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
|
|
1592
|
+
}
|
|
1593
|
+
out.log(` files: bounded at attempt ${attempt}, resuming\u2026`);
|
|
1594
|
+
}
|
|
1595
|
+
throw new Error("file copy did not finish within 20 rounds \u2014 re-run to continue where it left off");
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1371
1598
|
// src/app-lifecycle.ts
|
|
1372
1599
|
async function lifecycleCall(action, options) {
|
|
1373
1600
|
const cfg = await loadProjectConfig(options.configPath);
|
|
@@ -1429,9 +1656,48 @@ async function appCommand(parsed, dependencies = {}) {
|
|
|
1429
1656
|
});
|
|
1430
1657
|
return;
|
|
1431
1658
|
}
|
|
1659
|
+
if (sub === "refresh-sandbox" || sub === "go-live" || sub === "promote") {
|
|
1660
|
+
assertArgs(parsed, ["config", "include-identity", "include-files", "dry-run", "yes", "token", "email", "json"], 2);
|
|
1661
|
+
await appTransfer({
|
|
1662
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1663
|
+
verb: sub,
|
|
1664
|
+
includeIdentity: parsed.options["include-identity"] === true,
|
|
1665
|
+
includeFiles: parsed.options["include-files"] === true,
|
|
1666
|
+
dryRun: parsed.options["dry-run"] === true,
|
|
1667
|
+
yes: parsed.options.yes === true,
|
|
1668
|
+
token: stringOpt(parsed.options.token),
|
|
1669
|
+
email: stringOpt(parsed.options.email),
|
|
1670
|
+
json: parsed.options.json === true,
|
|
1671
|
+
fetch: dependencies.fetch,
|
|
1672
|
+
stdout: dependencies.stdout
|
|
1673
|
+
});
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
if (sub === "import") {
|
|
1677
|
+
assertArgs(parsed, ["config", "env", "ns", "id-field", "key", "generate-ids", "dry-run", "yes", "token", "email", "json"], 3);
|
|
1678
|
+
const file = parsed.positionals[2];
|
|
1679
|
+
if (!file) throw new Error('app import needs a file: "odla-ai app import rows.json --ns todos --yes" (or "-" for stdin)');
|
|
1680
|
+
await appImport({
|
|
1681
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1682
|
+
file,
|
|
1683
|
+
env: stringOpt(parsed.options.env),
|
|
1684
|
+
ns: stringOpt(parsed.options.ns),
|
|
1685
|
+
idField: stringOpt(parsed.options["id-field"]),
|
|
1686
|
+
key: stringOpt(parsed.options.key),
|
|
1687
|
+
generateIds: parsed.options["generate-ids"] === true,
|
|
1688
|
+
dryRun: parsed.options["dry-run"] === true,
|
|
1689
|
+
yes: parsed.options.yes === true,
|
|
1690
|
+
token: stringOpt(parsed.options.token),
|
|
1691
|
+
email: stringOpt(parsed.options.email),
|
|
1692
|
+
json: parsed.options.json === true,
|
|
1693
|
+
fetch: dependencies.fetch,
|
|
1694
|
+
stdout: dependencies.stdout
|
|
1695
|
+
});
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1432
1698
|
if (sub !== "archive" && sub !== "restore") {
|
|
1433
1699
|
throw new Error(
|
|
1434
|
-
`unknown app subcommand "${sub ?? ""}". Try "odla-ai app archive --yes", "odla-ai app restore", "odla-ai app export", or "odla-ai app owners <list|add|remove>". (Permanent deletion has no CLI: it requires a signed-in owner in Studio.)`
|
|
1700
|
+
`unknown app subcommand "${sub ?? ""}". Try "odla-ai app archive --yes", "odla-ai app restore", "odla-ai app export", "odla-ai app import <file>", "odla-ai app refresh-sandbox", "odla-ai app go-live", "odla-ai app promote", or "odla-ai app owners <list|add|remove>". (Permanent deletion has no CLI: it requires a signed-in owner in Studio.)`
|
|
1435
1701
|
);
|
|
1436
1702
|
}
|
|
1437
1703
|
assertArgs(parsed, ["config", "token", "email", "yes", "json"], 2);
|
|
@@ -1473,7 +1739,7 @@ var CAPABILITIES = {
|
|
|
1473
1739
|
human: [
|
|
1474
1740
|
"provide the existing odla account email, then sign in and explicitly review/approve the exact device code",
|
|
1475
1741
|
"review mirrored calendar/attendee disclosure and complete the server-issued Google consent flow",
|
|
1476
|
-
"consent to
|
|
1742
|
+
"consent to changes against an app's live database with --yes",
|
|
1477
1743
|
"request destructive credential rotation explicitly",
|
|
1478
1744
|
"review application semantics, security findings, releases, and merges",
|
|
1479
1745
|
"approve redacted-source disclosure before hosted security reasoning",
|
|
@@ -1938,7 +2204,7 @@ function printStatus(status, json, out) {
|
|
|
1938
2204
|
}
|
|
1939
2205
|
|
|
1940
2206
|
// src/code-connect.ts
|
|
1941
|
-
var
|
|
2207
|
+
var import_node_fs8 = require("fs");
|
|
1942
2208
|
var import_node_os2 = require("os");
|
|
1943
2209
|
var import_node_path5 = require("path");
|
|
1944
2210
|
|
|
@@ -4825,9 +5091,9 @@ var CodePiRuntimeEngine = class {
|
|
|
4825
5091
|
};
|
|
4826
5092
|
|
|
4827
5093
|
// src/help.ts
|
|
4828
|
-
var
|
|
5094
|
+
var import_node_fs7 = require("fs");
|
|
4829
5095
|
function cliVersion() {
|
|
4830
|
-
const pkg = JSON.parse((0,
|
|
5096
|
+
const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
4831
5097
|
return pkg.version ?? "unknown";
|
|
4832
5098
|
}
|
|
4833
5099
|
function printHelp() {
|
|
@@ -4844,6 +5110,10 @@ Usage:
|
|
|
4844
5110
|
odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
|
|
4845
5111
|
odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
4846
5112
|
odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
|
|
5113
|
+
odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
|
|
5114
|
+
odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
|
|
5115
|
+
odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
|
|
5116
|
+
odla-ai app promote [--dry-run] [--json] --yes
|
|
4847
5117
|
odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
4848
5118
|
odla-ai app owners add <email> [--email <odla-account>] [--json]
|
|
4849
5119
|
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
@@ -4880,17 +5150,29 @@ Commands:
|
|
|
4880
5150
|
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
4881
5151
|
doctor Validate and summarize the project config without network calls.
|
|
4882
5152
|
calendar Inspect, connect, or disconnect the live Google booking connection.
|
|
4883
|
-
app Archive (suspend, data retained), restore, export, or
|
|
4884
|
-
co-owners of the app. Archiving takes every
|
|
4885
|
-
plane down until restored; permanent deletion
|
|
4886
|
-
requires a signed-in owner in Studio, and no
|
|
4887
|
-
credential can purge. "app export" downloads a
|
|
4888
|
-
gzipped-JSONL snapshot of one
|
|
4889
|
-
|
|
5153
|
+
app Archive (suspend, data retained), restore, export, import, or
|
|
5154
|
+
manage the co-owners of the app. Archiving takes every
|
|
5155
|
+
environment's data plane down until restored; permanent deletion
|
|
5156
|
+
has NO CLI \u2014 it requires a signed-in owner in Studio, and no
|
|
5157
|
+
machine or agent credential can purge. "app export" downloads a
|
|
5158
|
+
portable gzipped-JSONL snapshot of one database (--fresh takes a
|
|
5159
|
+
new snapshot first) \u2014 your data is always yours to take.
|
|
5160
|
+
"app import" upserts rows back in from JSON, JSONL, or a
|
|
5161
|
+
{namespace: rows} map (the shape a query returns); it writes only
|
|
5162
|
+
with --yes, so the bare command is a dry run, and it refuses to
|
|
5163
|
+
guess an id mode that would duplicate rows on a re-run.
|
|
5164
|
+
"app refresh-sandbox" replaces the sandbox with a copy of live
|
|
5165
|
+
(the routine dev loop); "app go-live" copies the sandbox into an
|
|
5166
|
+
EMPTY live database, once, at launch; "app promote" pushes only
|
|
5167
|
+
schema, rules and gates up afterwards, leaving live's rows alone.
|
|
5168
|
+
Each prints its plan and writes nothing without --yes, so the
|
|
5169
|
+
bare command is the dry run. Clerk config, triggers, allowlists,
|
|
5170
|
+
secrets and API keys never travel; identity rows stay behind
|
|
5171
|
+
unless --include-identity.
|
|
4890
5172
|
"app owners add <email>" grants a signed-up odla member the SAME
|
|
4891
5173
|
full access as you; they then run their own "provision" to mint
|
|
4892
|
-
their own credentials for the shared db (
|
|
4893
|
-
secret is ever copied between people.
|
|
5174
|
+
their own credentials for the shared db (the live one included)
|
|
5175
|
+
\u2014 no secret is ever copied between people.
|
|
4894
5176
|
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
4895
5177
|
code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
|
|
4896
5178
|
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
@@ -4905,8 +5187,11 @@ Commands:
|
|
|
4905
5187
|
version Print the CLI version.
|
|
4906
5188
|
|
|
4907
5189
|
Safety:
|
|
4908
|
-
|
|
4909
|
-
|
|
5190
|
+
Every app has two databases on odla.ai: a sandbox (env "dev", tenant
|
|
5191
|
+
<appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
|
|
5192
|
+
production odla.ai \u2014 there is no separate odla to point at. New projects get
|
|
5193
|
+
the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
|
|
5194
|
+
provision the live database; use --dry-run first to inspect the resolved plan.
|
|
4910
5195
|
Provision caches the approved developer token and service credentials under
|
|
4911
5196
|
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
4912
5197
|
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
@@ -5321,7 +5606,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
|
|
|
5321
5606
|
async function codeConnect(options) {
|
|
5322
5607
|
const cwd = options.cwd ?? process.cwd();
|
|
5323
5608
|
const configPath = (0, import_node_path5.resolve)(cwd, options.configPath);
|
|
5324
|
-
const cfg = (0,
|
|
5609
|
+
const cfg = (0, import_node_fs8.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
|
|
5325
5610
|
const requestedAppId = options.appId?.trim();
|
|
5326
5611
|
if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
|
|
5327
5612
|
throw new Error("--app-id must be a valid odla app id");
|
|
@@ -5331,9 +5616,9 @@ async function codeConnect(options) {
|
|
|
5331
5616
|
}
|
|
5332
5617
|
const appId = requestedAppId ?? cfg?.app.id;
|
|
5333
5618
|
const platform = (options.platform ?? cfg?.platformUrl ?? process.env.ODLA_PLATFORM_URL ?? "https://odla.ai").replace(/\/+$/, "");
|
|
5334
|
-
const appEnv = options.env ?? (cfg ? cfg.envs.includes("dev") ? "dev" : cfg.envs[0] :
|
|
5619
|
+
const appEnv = options.env ?? (cfg ? cfg.envs.includes("dev") ? "dev" : cfg.envs[0] : void 0);
|
|
5335
5620
|
if (appEnv !== "dev" && appEnv !== "prod" || cfg && !cfg.envs.includes(appEnv)) {
|
|
5336
|
-
throw new Error(cfg ? `Code env "${appEnv ?? ""}" must be dev or prod and declared in ${options.configPath}` : `Code env "${appEnv ?? ""}" must be dev or prod`);
|
|
5621
|
+
throw new Error(cfg ? `Code env "${appEnv ?? ""}" must be dev or prod and declared in ${options.configPath}` : `Code env "${appEnv ?? ""}" must be dev or prod; pass --env explicitly when there is no ${options.configPath}`);
|
|
5337
5622
|
}
|
|
5338
5623
|
if (process.platform !== "darwin" && process.platform !== "linux") {
|
|
5339
5624
|
throw new Error("odla Code hosts require macOS or Linux");
|
|
@@ -5540,12 +5825,12 @@ async function codeCommand(parsed, dependencies) {
|
|
|
5540
5825
|
|
|
5541
5826
|
// src/doctor-checks.ts
|
|
5542
5827
|
var import_node_child_process6 = require("child_process");
|
|
5543
|
-
var
|
|
5828
|
+
var import_node_fs10 = require("fs");
|
|
5544
5829
|
var import_node_path7 = require("path");
|
|
5545
5830
|
|
|
5546
5831
|
// src/wrangler.ts
|
|
5547
5832
|
var import_node_child_process5 = require("child_process");
|
|
5548
|
-
var
|
|
5833
|
+
var import_node_fs9 = require("fs");
|
|
5549
5834
|
var import_node_path6 = require("path");
|
|
5550
5835
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
5551
5836
|
const child = (0, import_node_child_process5.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -5561,14 +5846,14 @@ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
|
|
|
5561
5846
|
function findWranglerConfig(rootDir) {
|
|
5562
5847
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
5563
5848
|
const path = (0, import_node_path6.join)(rootDir, name);
|
|
5564
|
-
if ((0,
|
|
5849
|
+
if ((0, import_node_fs9.existsSync)(path)) return path;
|
|
5565
5850
|
}
|
|
5566
5851
|
return null;
|
|
5567
5852
|
}
|
|
5568
5853
|
function readWranglerConfig(path) {
|
|
5569
5854
|
if (path.endsWith(".toml")) return null;
|
|
5570
5855
|
try {
|
|
5571
|
-
return JSON.parse(stripJsonComments((0,
|
|
5856
|
+
return JSON.parse(stripJsonComments((0, import_node_fs9.readFileSync)(path, "utf8")));
|
|
5572
5857
|
} catch {
|
|
5573
5858
|
return null;
|
|
5574
5859
|
}
|
|
@@ -5669,7 +5954,7 @@ function wranglerWarnings(rootDir) {
|
|
|
5669
5954
|
const dir = (0, import_node_path7.resolve)(rootDir, assets.directory);
|
|
5670
5955
|
if (dir === (0, import_node_path7.resolve)(rootDir)) {
|
|
5671
5956
|
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
5672
|
-
} else if ((0,
|
|
5957
|
+
} else if ((0, import_node_fs10.existsSync)((0, import_node_path7.join)(dir, "node_modules"))) {
|
|
5673
5958
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
5674
5959
|
}
|
|
5675
5960
|
}
|
|
@@ -5705,12 +5990,12 @@ function o11yProjectWarnings(rootDir) {
|
|
|
5705
5990
|
return warnings;
|
|
5706
5991
|
}
|
|
5707
5992
|
const main = typeof config.main === "string" ? (0, import_node_path7.resolve)(rootDir, config.main) : null;
|
|
5708
|
-
if (!main || !(0,
|
|
5993
|
+
if (!main || !(0, import_node_fs10.existsSync)(main)) {
|
|
5709
5994
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
5710
5995
|
} else {
|
|
5711
5996
|
let source = "";
|
|
5712
5997
|
try {
|
|
5713
|
-
source = (0,
|
|
5998
|
+
source = (0, import_node_fs10.readFileSync)(main, "utf8");
|
|
5714
5999
|
} catch {
|
|
5715
6000
|
}
|
|
5716
6001
|
if (!/\bwithObservability\b/.test(source)) {
|
|
@@ -5734,7 +6019,7 @@ function calendarProjectWarnings(rootDir) {
|
|
|
5734
6019
|
}
|
|
5735
6020
|
function readPackageJson(rootDir) {
|
|
5736
6021
|
try {
|
|
5737
|
-
return JSON.parse((0,
|
|
6022
|
+
return JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path7.join)(rootDir, "package.json"), "utf8"));
|
|
5738
6023
|
} catch {
|
|
5739
6024
|
return null;
|
|
5740
6025
|
}
|
|
@@ -5935,13 +6220,13 @@ function harnessOption(value, flag) {
|
|
|
5935
6220
|
}
|
|
5936
6221
|
|
|
5937
6222
|
// src/init.ts
|
|
5938
|
-
var
|
|
6223
|
+
var import_node_fs11 = require("fs");
|
|
5939
6224
|
var import_node_path8 = require("path");
|
|
5940
6225
|
function initProject(options) {
|
|
5941
6226
|
const out = options.stdout ?? console;
|
|
5942
6227
|
const rootDir = (0, import_node_path8.resolve)(options.rootDir ?? process.cwd());
|
|
5943
6228
|
const configPath = (0, import_node_path8.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
5944
|
-
if ((0,
|
|
6229
|
+
if ((0, import_node_fs11.existsSync)(configPath) && !options.force) {
|
|
5945
6230
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
5946
6231
|
}
|
|
5947
6232
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
@@ -5951,10 +6236,10 @@ function initProject(options) {
|
|
|
5951
6236
|
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
5952
6237
|
if (services.includes("calendar") && !services.includes("db")) throw new Error("--services calendar requires db");
|
|
5953
6238
|
const aiProvider = options.aiProvider ?? "anthropic";
|
|
5954
|
-
(0,
|
|
5955
|
-
(0,
|
|
5956
|
-
(0,
|
|
5957
|
-
(0,
|
|
6239
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path8.dirname)(configPath), { recursive: true });
|
|
6240
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path8.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
6241
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path8.resolve)(rootDir, ".odla"), { recursive: true });
|
|
6242
|
+
(0, import_node_fs11.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
5958
6243
|
writeIfMissing((0, import_node_path8.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
5959
6244
|
writeIfMissing((0, import_node_path8.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
5960
6245
|
ensureGitignore(rootDir);
|
|
@@ -5963,8 +6248,8 @@ function initProject(options) {
|
|
|
5963
6248
|
out.log("updated .gitignore for local odla credentials");
|
|
5964
6249
|
}
|
|
5965
6250
|
function writeIfMissing(path, text) {
|
|
5966
|
-
if ((0,
|
|
5967
|
-
(0,
|
|
6251
|
+
if ((0, import_node_fs11.existsSync)(path)) return;
|
|
6252
|
+
(0, import_node_fs11.writeFileSync)(path, text);
|
|
5968
6253
|
}
|
|
5969
6254
|
function configTemplate(input) {
|
|
5970
6255
|
const calendar = input.services.includes("calendar") ? ` calendar: {
|
|
@@ -6065,7 +6350,7 @@ function relativeDisplay(path, rootDir) {
|
|
|
6065
6350
|
}
|
|
6066
6351
|
|
|
6067
6352
|
// src/provision.ts
|
|
6068
|
-
var
|
|
6353
|
+
var import_apps4 = require("@odla-ai/apps");
|
|
6069
6354
|
var import_ai2 = require("@odla-ai/ai");
|
|
6070
6355
|
var import_node_process7 = __toESM(require("process"), 1);
|
|
6071
6356
|
|
|
@@ -6122,9 +6407,9 @@ async function responseText(res) {
|
|
|
6122
6407
|
}
|
|
6123
6408
|
|
|
6124
6409
|
// src/provision-credentials.ts
|
|
6125
|
-
var
|
|
6410
|
+
var import_apps2 = require("@odla-ai/apps");
|
|
6126
6411
|
async function provisionEnvCredentials(opts) {
|
|
6127
|
-
const tenantId = (0,
|
|
6412
|
+
const tenantId = (0, import_apps2.tenantIdFor)(opts.cfg.app.id, opts.env);
|
|
6128
6413
|
const prior = opts.credentials?.envs[opts.env];
|
|
6129
6414
|
let credentials = opts.credentials;
|
|
6130
6415
|
let dbKey = opts.cfg.services.includes("db") && !opts.rotateDb ? prior?.dbKey : void 0;
|
|
@@ -6218,7 +6503,7 @@ async function safeText3(res) {
|
|
|
6218
6503
|
|
|
6219
6504
|
// src/provision-helpers.ts
|
|
6220
6505
|
var import_ai = require("@odla-ai/ai");
|
|
6221
|
-
var
|
|
6506
|
+
var import_apps3 = require("@odla-ai/apps");
|
|
6222
6507
|
function serviceRank(service) {
|
|
6223
6508
|
if (service === "db") return 0;
|
|
6224
6509
|
if (service === "calendar") return 2;
|
|
@@ -6229,7 +6514,7 @@ function defaultSecretName(provider) {
|
|
|
6229
6514
|
return names[provider] ?? `${provider}_api_key`;
|
|
6230
6515
|
}
|
|
6231
6516
|
async function assertTenantAdminAccess(doFetch, cfg, env, token) {
|
|
6232
|
-
const tenantId = (0,
|
|
6517
|
+
const tenantId = (0, import_apps3.tenantIdFor)(cfg.app.id, env);
|
|
6233
6518
|
const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/entitlements`, {
|
|
6234
6519
|
headers: { authorization: `Bearer ${token}` }
|
|
6235
6520
|
});
|
|
@@ -6372,7 +6657,7 @@ async function provision(options) {
|
|
|
6372
6657
|
const productionEnvs = plan.envs.filter((env) => env === "prod" || env === "production");
|
|
6373
6658
|
if (!options.dryRun && productionEnvs.length > 0 && !options.yes) {
|
|
6374
6659
|
throw new Error(
|
|
6375
|
-
`refusing to provision
|
|
6660
|
+
`refusing to provision the live database (env ${productionEnvs.join(", ")}) without --yes; run --dry-run first`
|
|
6376
6661
|
);
|
|
6377
6662
|
}
|
|
6378
6663
|
const database = await resolveDatabaseConfig(cfg);
|
|
@@ -6420,7 +6705,7 @@ async function provision(options) {
|
|
|
6420
6705
|
}
|
|
6421
6706
|
const doFetch = options.fetch ?? fetch;
|
|
6422
6707
|
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
6423
|
-
const apps = (0,
|
|
6708
|
+
const apps = (0, import_apps4.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
|
|
6424
6709
|
const existing = await readRegistryApp(cfg, token, doFetch);
|
|
6425
6710
|
if (existing) {
|
|
6426
6711
|
out.log(`app: ${cfg.app.id} already exists`);
|
|
@@ -6464,7 +6749,7 @@ async function provision(options) {
|
|
|
6464
6749
|
}
|
|
6465
6750
|
}
|
|
6466
6751
|
for (const env of cfg.envs) {
|
|
6467
|
-
const tenantId = (0,
|
|
6752
|
+
const tenantId = (0, import_apps4.tenantIdFor)(cfg.app.id, env);
|
|
6468
6753
|
credentials = await provisionEnvCredentials({
|
|
6469
6754
|
cfg,
|
|
6470
6755
|
env,
|
|
@@ -6550,7 +6835,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
6550
6835
|
|
|
6551
6836
|
// src/secrets-set.ts
|
|
6552
6837
|
var import_ai3 = require("@odla-ai/ai");
|
|
6553
|
-
var
|
|
6838
|
+
var import_apps5 = require("@odla-ai/apps");
|
|
6554
6839
|
var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
|
|
6555
6840
|
async function secretsSet(options) {
|
|
6556
6841
|
const name = (options.name ?? "").trim();
|
|
@@ -6602,7 +6887,7 @@ async function resolveVaultWrite(options) {
|
|
|
6602
6887
|
throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
|
|
6603
6888
|
}
|
|
6604
6889
|
const value = await secretInputValue(options, "secret");
|
|
6605
|
-
return { cfg, tenantId: (0,
|
|
6890
|
+
return { cfg, tenantId: (0, import_apps5.tenantIdFor)(cfg.app.id, options.env), value, doFetch, out };
|
|
6606
6891
|
}
|
|
6607
6892
|
|
|
6608
6893
|
// src/security-command-context.ts
|
|
@@ -7294,7 +7579,7 @@ async function securityStatus(parsed, dependencies) {
|
|
|
7294
7579
|
}
|
|
7295
7580
|
|
|
7296
7581
|
// src/skill.ts
|
|
7297
|
-
var
|
|
7582
|
+
var import_node_fs12 = require("fs");
|
|
7298
7583
|
var import_node_os3 = require("os");
|
|
7299
7584
|
var import_node_path10 = require("path");
|
|
7300
7585
|
var import_node_url3 = require("url");
|
|
@@ -7372,7 +7657,7 @@ function installSkill(options = {}) {
|
|
|
7372
7657
|
plans.set(target, { target, content, boundary, managedMerge });
|
|
7373
7658
|
};
|
|
7374
7659
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
7375
|
-
for (const rel of files) plan((0, import_node_path10.join)(targetDir2, rel), (0,
|
|
7660
|
+
for (const rel of files) plan((0, import_node_path10.join)(targetDir2, rel), (0, import_node_fs12.readFileSync)((0, import_node_path10.join)(sourceDir, rel), "utf8"), false, boundary);
|
|
7376
7661
|
};
|
|
7377
7662
|
let targetDir;
|
|
7378
7663
|
if (options.global) {
|
|
@@ -7392,7 +7677,7 @@ function installSkill(options = {}) {
|
|
|
7392
7677
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
7393
7678
|
if (harnesses.includes("claude")) {
|
|
7394
7679
|
for (const skill of skillNames(files)) {
|
|
7395
|
-
const canonical = (0,
|
|
7680
|
+
const canonical = (0, import_node_fs12.readFileSync)((0, import_node_path10.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
7396
7681
|
plan((0, import_node_path10.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
7397
7682
|
}
|
|
7398
7683
|
rememberTarget("claude", claudeRoot);
|
|
@@ -7427,11 +7712,11 @@ function installSkill(options = {}) {
|
|
|
7427
7712
|
conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
|
|
7428
7713
|
continue;
|
|
7429
7714
|
}
|
|
7430
|
-
if (!(0,
|
|
7715
|
+
if (!(0, import_node_fs12.existsSync)(file.target)) {
|
|
7431
7716
|
writtenPaths.add(file.target);
|
|
7432
7717
|
continue;
|
|
7433
7718
|
}
|
|
7434
|
-
const current = (0,
|
|
7719
|
+
const current = (0, import_node_fs12.readFileSync)(file.target, "utf8");
|
|
7435
7720
|
if (current === file.content) {
|
|
7436
7721
|
unchangedPaths.add(file.target);
|
|
7437
7722
|
} else if (file.managedMerge || options.force) {
|
|
@@ -7448,9 +7733,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
7448
7733
|
);
|
|
7449
7734
|
}
|
|
7450
7735
|
for (const file of plans.values()) {
|
|
7451
|
-
if (!(0,
|
|
7452
|
-
(0,
|
|
7453
|
-
(0,
|
|
7736
|
+
if (!(0, import_node_fs12.existsSync)(file.target) || (0, import_node_fs12.readFileSync)(file.target, "utf8") !== file.content) {
|
|
7737
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path10.dirname)(file.target), { recursive: true });
|
|
7738
|
+
(0, import_node_fs12.writeFileSync)(file.target, file.content);
|
|
7454
7739
|
}
|
|
7455
7740
|
}
|
|
7456
7741
|
const skills = skillNames(files);
|
|
@@ -7491,9 +7776,9 @@ function normalizeHarnesses(values, global) {
|
|
|
7491
7776
|
function managedFileContent(path, block, force, boundary) {
|
|
7492
7777
|
const symlink = symlinkedComponent(boundary, path);
|
|
7493
7778
|
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
7494
|
-
if (!(0,
|
|
7779
|
+
if (!(0, import_node_fs12.existsSync)(path)) return `${block}
|
|
7495
7780
|
`;
|
|
7496
|
-
const current = (0,
|
|
7781
|
+
const current = (0, import_node_fs12.readFileSync)(path, "utf8");
|
|
7497
7782
|
const start = "<!-- odla-ai agent setup:start -->";
|
|
7498
7783
|
const end = "<!-- odla-ai agent setup:end -->";
|
|
7499
7784
|
const startAt = current.indexOf(start);
|
|
@@ -7522,7 +7807,7 @@ function symlinkedComponent(boundary, target) {
|
|
|
7522
7807
|
for (const part of rel.split(import_node_path10.sep).filter(Boolean)) {
|
|
7523
7808
|
current = (0, import_node_path10.join)(current, part);
|
|
7524
7809
|
try {
|
|
7525
|
-
if ((0,
|
|
7810
|
+
if ((0, import_node_fs12.lstatSync)(current).isSymbolicLink()) return current;
|
|
7526
7811
|
} catch (error) {
|
|
7527
7812
|
if (error.code !== "ENOENT") throw error;
|
|
7528
7813
|
}
|
|
@@ -7533,10 +7818,10 @@ function skillNames(files) {
|
|
|
7533
7818
|
return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
|
|
7534
7819
|
}
|
|
7535
7820
|
function listFiles(dir) {
|
|
7536
|
-
if (!(0,
|
|
7821
|
+
if (!(0, import_node_fs12.existsSync)(dir)) return [];
|
|
7537
7822
|
const results = [];
|
|
7538
7823
|
const walk = (current) => {
|
|
7539
|
-
for (const entry of (0,
|
|
7824
|
+
for (const entry of (0, import_node_fs12.readdirSync)(current, { withFileTypes: true })) {
|
|
7540
7825
|
const path = (0, import_node_path10.join)(current, entry.name);
|
|
7541
7826
|
if (entry.isDirectory()) walk(path);
|
|
7542
7827
|
else results.push((0, import_node_path10.relative)(dir, path));
|