@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/index.cjs
CHANGED
|
@@ -1293,14 +1293,36 @@ function unique2(values) {
|
|
|
1293
1293
|
return [...new Set(values.filter(Boolean))];
|
|
1294
1294
|
}
|
|
1295
1295
|
|
|
1296
|
+
// src/tenant.ts
|
|
1297
|
+
var import_apps = require("@odla-ai/apps");
|
|
1298
|
+
function resolveEnv(cfg, requested) {
|
|
1299
|
+
const env = requested ?? (cfg.envs.includes("dev") ? "dev" : cfg.envs[0]);
|
|
1300
|
+
if (!env || !cfg.envs.includes(env)) {
|
|
1301
|
+
throw new Error(`env "${env ?? ""}" is not declared in ${cfg.configPath}`);
|
|
1302
|
+
}
|
|
1303
|
+
return env;
|
|
1304
|
+
}
|
|
1305
|
+
function resolveTenant(cfg, requested) {
|
|
1306
|
+
const env = resolveEnv(cfg, requested);
|
|
1307
|
+
return { env, tenant: (0, import_apps.tenantIdFor)(cfg.app.id, env) };
|
|
1308
|
+
}
|
|
1309
|
+
function bothTenants(cfg) {
|
|
1310
|
+
for (const env of ["dev", "prod"]) {
|
|
1311
|
+
if (!cfg.envs.includes(env)) {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
`env "${env}" is not declared in ${cfg.configPath} \u2014 add it to \`envs\` and run \`odla-ai provision --yes\` before transferring data between environments`
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
return { sandbox: (0, import_apps.tenantIdFor)(cfg.app.id, "dev"), live: (0, import_apps.tenantIdFor)(cfg.app.id, "prod") };
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1296
1320
|
// src/app-export.ts
|
|
1297
1321
|
async function appExport(options) {
|
|
1298
1322
|
const cfg = await loadProjectConfig(options.configPath);
|
|
1299
1323
|
const out = options.stdout ?? console;
|
|
1300
1324
|
const doFetch = options.fetch ?? fetch;
|
|
1301
|
-
const
|
|
1302
|
-
if (!env || !cfg.envs.includes(env)) throw new Error(`env "${env ?? ""}" is not declared in ${options.configPath}`);
|
|
1303
|
-
const tenant = env === "prod" ? cfg.app.id : `${cfg.app.id}--${env}`;
|
|
1325
|
+
const { tenant } = resolveTenant(cfg, options.env);
|
|
1304
1326
|
const token = await getDeveloperToken(
|
|
1305
1327
|
cfg,
|
|
1306
1328
|
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
@@ -1335,6 +1357,84 @@ async function appExport(options) {
|
|
|
1335
1357
|
return { file, backup: newest };
|
|
1336
1358
|
}
|
|
1337
1359
|
|
|
1360
|
+
// src/app-import.ts
|
|
1361
|
+
var import_node_fs6 = require("fs");
|
|
1362
|
+
var import_import = require("@odla-ai/db/import");
|
|
1363
|
+
function chooseIdMode(options, rows) {
|
|
1364
|
+
const chosen = [options.idField && "field", options.key && "key", options.generateIds && "generate"].filter(Boolean);
|
|
1365
|
+
if (chosen.length > 1) throw new Error("choose one of --id-field, --key, or --generate-ids");
|
|
1366
|
+
if (options.key) return { idMode: "key" };
|
|
1367
|
+
if (options.generateIds) return { idMode: "generate" };
|
|
1368
|
+
if (options.idField) return { idMode: "field", idField: options.idField };
|
|
1369
|
+
if (rows.length > 0 && rows.every((row) => typeof row?.id === "string" && row.id)) return { idMode: "field" };
|
|
1370
|
+
throw new Error(
|
|
1371
|
+
"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)"
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
async function appImport(options) {
|
|
1375
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
1376
|
+
const out = options.stdout ?? console;
|
|
1377
|
+
const doFetch = options.fetch ?? fetch;
|
|
1378
|
+
const { tenant } = resolveTenant(cfg, options.env);
|
|
1379
|
+
const text = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs6.readFileSync)(0, "utf8")))() : (0, import_node_fs6.readFileSync)(options.file, "utf8");
|
|
1380
|
+
const { format, sources } = (0, import_import.parseImport)(text, options.ns);
|
|
1381
|
+
if (format === "namespace-map" && options.ns) {
|
|
1382
|
+
throw new Error("--ns cannot be combined with a {namespace: rows} file \u2014 the file already names each namespace");
|
|
1383
|
+
}
|
|
1384
|
+
if (format !== "namespace-map" && !options.ns) throw new Error("--ns is required unless the file is a {namespace: rows} map");
|
|
1385
|
+
if (sources.length === 0) throw new Error(`${options.file} contains no rows`);
|
|
1386
|
+
const mode = chooseIdMode(options, sources.map((s) => s.row));
|
|
1387
|
+
const build = { ...mode, keyAttr: options.key };
|
|
1388
|
+
const { ops, rejected, ignored } = (0, import_import.buildImportOps)(sources, build);
|
|
1389
|
+
if (rejected.length > 0) {
|
|
1390
|
+
const detail = rejected.slice(0, 10).map((r) => ` row ${r.index}: ${r.reason}`).join("\n");
|
|
1391
|
+
const more = rejected.length > 10 ? `
|
|
1392
|
+
\u2026and ${rejected.length - 10} more` : "";
|
|
1393
|
+
throw new Error(`${rejected.length} row(s) cannot be imported \u2014 fix the input and re-run:
|
|
1394
|
+
${detail}${more}`);
|
|
1395
|
+
}
|
|
1396
|
+
const chunks = (0, import_import.planImportChunks)(ops);
|
|
1397
|
+
const result = { tenant, rows: ops.length, chunks: chunks.length, committed: 0, duplicate: 0, txIds: [], ignored };
|
|
1398
|
+
if (ignored.length > 0) out.log(`ignoring server-maintained field(s): ${ignored.join(", ")} \u2014 odla-db stamps these on write`);
|
|
1399
|
+
if (options.dryRun || options.yes !== true) {
|
|
1400
|
+
const why = options.dryRun ? "--dry-run" : "no --yes";
|
|
1401
|
+
out.log(`${tenant}: would upsert ${ops.length} row(s) across ${chunks.length} transaction(s) \u2014 nothing written (${why})`);
|
|
1402
|
+
if (options.json) out.log(JSON.stringify(result, null, 2));
|
|
1403
|
+
return result;
|
|
1404
|
+
}
|
|
1405
|
+
const token = await getDeveloperToken(
|
|
1406
|
+
cfg,
|
|
1407
|
+
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1408
|
+
doFetch,
|
|
1409
|
+
out
|
|
1410
|
+
);
|
|
1411
|
+
const runId = crypto.randomUUID();
|
|
1412
|
+
const url = `${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenant)}/transact`;
|
|
1413
|
+
for (const chunk of chunks) {
|
|
1414
|
+
const res = await doFetch(url, {
|
|
1415
|
+
method: "POST",
|
|
1416
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
1417
|
+
body: JSON.stringify({ mutationId: (0, import_import.importMutationId)(runId, chunk.index), ops: chunk.ops })
|
|
1418
|
+
});
|
|
1419
|
+
const body = await res.json().catch(() => ({}));
|
|
1420
|
+
if (!res.ok) {
|
|
1421
|
+
const code = body.error?.code ? ` (${body.error.code})` : "";
|
|
1422
|
+
throw new Error(
|
|
1423
|
+
`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.`
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
result.committed += chunk.ops.length;
|
|
1427
|
+
if (typeof body.txId === "number") result.txIds.push(body.txId);
|
|
1428
|
+
if (body.duplicate) result.duplicate++;
|
|
1429
|
+
}
|
|
1430
|
+
if (options.json) out.log(JSON.stringify(result, null, 2));
|
|
1431
|
+
else {
|
|
1432
|
+
const dup = result.duplicate > 0 ? ` (${result.duplicate} chunk(s) were already applied)` : "";
|
|
1433
|
+
out.log(`${tenant}: upserted ${result.committed} row(s) in ${chunks.length} transaction(s)${dup}`);
|
|
1434
|
+
}
|
|
1435
|
+
return result;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1338
1438
|
// src/app-owners.ts
|
|
1339
1439
|
var sink = (options) => options.stdout ?? console;
|
|
1340
1440
|
async function ownersRequest(method, suffix, options, body) {
|
|
@@ -1422,6 +1522,133 @@ async function appOwnersCommand(parsed, dependencies = {}) {
|
|
|
1422
1522
|
);
|
|
1423
1523
|
}
|
|
1424
1524
|
|
|
1525
|
+
// src/app-transfer.ts
|
|
1526
|
+
function endpointsFor(verb, tenants) {
|
|
1527
|
+
const up = { source: tenants.sandbox, target: tenants.live, from: "dev" };
|
|
1528
|
+
const down = { source: tenants.live, target: tenants.sandbox, from: "prod" };
|
|
1529
|
+
if (verb === "refresh-sandbox") return { ...down, mode: "refresh" };
|
|
1530
|
+
return { ...up, mode: "cutover" };
|
|
1531
|
+
}
|
|
1532
|
+
async function api(cfg, token, path, init, doFetch) {
|
|
1533
|
+
const res = await doFetch(`${cfg.dbEndpoint}${path}`, {
|
|
1534
|
+
...init,
|
|
1535
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json", ...init?.headers }
|
|
1536
|
+
});
|
|
1537
|
+
return { status: res.status, body: await res.json().catch(() => ({})) };
|
|
1538
|
+
}
|
|
1539
|
+
function describe(side) {
|
|
1540
|
+
if (!side.exists) return "not provisioned";
|
|
1541
|
+
if (side.maxTx === 0) return "empty \u2014 never written";
|
|
1542
|
+
const ns = side.namespaces.length === 1 ? "1 namespace" : `${side.namespaces.length} namespaces`;
|
|
1543
|
+
const identity = side.identityRows > 0 ? `, ${side.identityRows} identity row(s)` : "";
|
|
1544
|
+
return `${ns} \xB7 ${side.triples} value(s) \xB7 tx ${side.maxTx}${identity}`;
|
|
1545
|
+
}
|
|
1546
|
+
function printPlan(out, verb, pre, opts) {
|
|
1547
|
+
const arrow = verb === "refresh-sandbox" ? "live \u2192 sandbox" : "sandbox \u2192 live";
|
|
1548
|
+
out.log(`${verb} (${arrow})`);
|
|
1549
|
+
out.log(` from ${pre.source.tenant} ${describe(pre.source)}`);
|
|
1550
|
+
out.log(` to ${pre.target.tenant} ${describe(pre.target)}`);
|
|
1551
|
+
if (verb === "promote") {
|
|
1552
|
+
out.log(" moves schema, rules and gates only \u2014 no rows are copied or removed");
|
|
1553
|
+
} else {
|
|
1554
|
+
out.log(` ${pre.target.tenant} is REPLACED by ${pre.source.tenant}`);
|
|
1555
|
+
out.log(` stays put: ${pre.staysPut.join(", ")}`);
|
|
1556
|
+
const identity = opts.includeIdentity ? "INCLUDED (--include-identity)" : `left behind: ${pre.excluded.join(", ")}`;
|
|
1557
|
+
out.log(` identity: ${identity}`);
|
|
1558
|
+
out.log(` files: ${opts.includeFiles ? "copied (--include-files)" : "not copied"}`);
|
|
1559
|
+
}
|
|
1560
|
+
for (const blocker of pre.blockers) out.log(` \u2716 ${blocker.code}: ${blocker.message}`);
|
|
1561
|
+
}
|
|
1562
|
+
async function appTransfer(options) {
|
|
1563
|
+
const cfg = await loadProjectConfig(options.configPath);
|
|
1564
|
+
const out = options.stdout ?? console;
|
|
1565
|
+
const doFetch = options.fetch ?? fetch;
|
|
1566
|
+
const tenants = bothTenants(cfg);
|
|
1567
|
+
const route2 = endpointsFor(options.verb, tenants);
|
|
1568
|
+
const token = await getDeveloperToken(
|
|
1569
|
+
cfg,
|
|
1570
|
+
{ configPath: cfg.configPath, token: options.token, email: options.email, open: false },
|
|
1571
|
+
doFetch,
|
|
1572
|
+
out
|
|
1573
|
+
);
|
|
1574
|
+
const pre = await api(
|
|
1575
|
+
cfg,
|
|
1576
|
+
token,
|
|
1577
|
+
`/admin/apps/${encodeURIComponent(route2.target)}/copy-preflight?from=${route2.from}`,
|
|
1578
|
+
void 0,
|
|
1579
|
+
doFetch
|
|
1580
|
+
);
|
|
1581
|
+
if (pre.status !== 200) {
|
|
1582
|
+
throw new Error(`pre-flight failed${pre.body.error?.code ? ` (${pre.body.error.code})` : ""}: ${pre.body.error?.message ?? pre.status}`);
|
|
1583
|
+
}
|
|
1584
|
+
const plan = pre.body;
|
|
1585
|
+
printPlan(out, options.verb, plan, options);
|
|
1586
|
+
if (options.verb === "go-live" && !plan.targetEmpty) {
|
|
1587
|
+
throw new Error(
|
|
1588
|
+
`${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.`
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
if (plan.blockers.length > 0) throw new Error(`cannot ${options.verb}: ${plan.blockers.map((b) => b.message).join("; ")}`);
|
|
1592
|
+
if (options.dryRun || options.yes !== true) {
|
|
1593
|
+
out.log(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
|
|
1594
|
+
if (options.json) out.log(JSON.stringify(plan, null, 2));
|
|
1595
|
+
return { ok: true, plan };
|
|
1596
|
+
}
|
|
1597
|
+
if (options.verb === "promote") {
|
|
1598
|
+
const res2 = await api(
|
|
1599
|
+
cfg,
|
|
1600
|
+
token,
|
|
1601
|
+
`/admin/apps/${encodeURIComponent(route2.target)}/promote-definitions`,
|
|
1602
|
+
{ method: "POST", body: JSON.stringify({ from: "dev" }) },
|
|
1603
|
+
doFetch
|
|
1604
|
+
);
|
|
1605
|
+
if (res2.status !== 200) throw new Error(`promote failed${res2.body.error?.code ? ` (${res2.body.error.code})` : ""}: ${res2.body.error?.message ?? res2.status}`);
|
|
1606
|
+
if (options.json) out.log(JSON.stringify(res2.body, null, 2));
|
|
1607
|
+
else out.log(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
|
|
1608
|
+
return { ok: true, plan, result: res2.body };
|
|
1609
|
+
}
|
|
1610
|
+
const res = await api(
|
|
1611
|
+
cfg,
|
|
1612
|
+
token,
|
|
1613
|
+
`/admin/apps/${encodeURIComponent(route2.target)}/copy-db`,
|
|
1614
|
+
{
|
|
1615
|
+
method: "POST",
|
|
1616
|
+
body: JSON.stringify({
|
|
1617
|
+
from: route2.from,
|
|
1618
|
+
mode: route2.mode,
|
|
1619
|
+
...options.includeIdentity ? { includeUsers: true } : {},
|
|
1620
|
+
...options.includeFiles ? { includeFiles: true } : {}
|
|
1621
|
+
})
|
|
1622
|
+
},
|
|
1623
|
+
doFetch
|
|
1624
|
+
);
|
|
1625
|
+
if (res.status !== 200) throw new Error(`${options.verb} failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
|
|
1626
|
+
out.log(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
|
|
1627
|
+
if (options.includeFiles) await copyFiles(cfg, token, route2, doFetch, out);
|
|
1628
|
+
if (options.json) out.log(JSON.stringify(res.body, null, 2));
|
|
1629
|
+
return { ok: true, plan, result: res.body };
|
|
1630
|
+
}
|
|
1631
|
+
async function copyFiles(cfg, token, route2, doFetch, out) {
|
|
1632
|
+
for (let attempt = 1; attempt <= 20; attempt++) {
|
|
1633
|
+
const res = await api(
|
|
1634
|
+
cfg,
|
|
1635
|
+
token,
|
|
1636
|
+
`/admin/apps/${encodeURIComponent(route2.target)}/copy-files`,
|
|
1637
|
+
{ method: "POST", body: JSON.stringify({ from: route2.from }) },
|
|
1638
|
+
doFetch
|
|
1639
|
+
);
|
|
1640
|
+
if (res.status === 200) {
|
|
1641
|
+
out.log(`${route2.target}: files copied`);
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
if (!res.body.error?.retry) {
|
|
1645
|
+
throw new Error(`file copy failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
|
|
1646
|
+
}
|
|
1647
|
+
out.log(` files: bounded at attempt ${attempt}, resuming\u2026`);
|
|
1648
|
+
}
|
|
1649
|
+
throw new Error("file copy did not finish within 20 rounds \u2014 re-run to continue where it left off");
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1425
1652
|
// src/app-lifecycle.ts
|
|
1426
1653
|
async function lifecycleCall(action, options) {
|
|
1427
1654
|
const cfg = await loadProjectConfig(options.configPath);
|
|
@@ -1483,9 +1710,48 @@ async function appCommand(parsed, dependencies = {}) {
|
|
|
1483
1710
|
});
|
|
1484
1711
|
return;
|
|
1485
1712
|
}
|
|
1713
|
+
if (sub === "refresh-sandbox" || sub === "go-live" || sub === "promote") {
|
|
1714
|
+
assertArgs(parsed, ["config", "include-identity", "include-files", "dry-run", "yes", "token", "email", "json"], 2);
|
|
1715
|
+
await appTransfer({
|
|
1716
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1717
|
+
verb: sub,
|
|
1718
|
+
includeIdentity: parsed.options["include-identity"] === true,
|
|
1719
|
+
includeFiles: parsed.options["include-files"] === true,
|
|
1720
|
+
dryRun: parsed.options["dry-run"] === true,
|
|
1721
|
+
yes: parsed.options.yes === true,
|
|
1722
|
+
token: stringOpt(parsed.options.token),
|
|
1723
|
+
email: stringOpt(parsed.options.email),
|
|
1724
|
+
json: parsed.options.json === true,
|
|
1725
|
+
fetch: dependencies.fetch,
|
|
1726
|
+
stdout: dependencies.stdout
|
|
1727
|
+
});
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
if (sub === "import") {
|
|
1731
|
+
assertArgs(parsed, ["config", "env", "ns", "id-field", "key", "generate-ids", "dry-run", "yes", "token", "email", "json"], 3);
|
|
1732
|
+
const file = parsed.positionals[2];
|
|
1733
|
+
if (!file) throw new Error('app import needs a file: "odla-ai app import rows.json --ns todos --yes" (or "-" for stdin)');
|
|
1734
|
+
await appImport({
|
|
1735
|
+
configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
|
|
1736
|
+
file,
|
|
1737
|
+
env: stringOpt(parsed.options.env),
|
|
1738
|
+
ns: stringOpt(parsed.options.ns),
|
|
1739
|
+
idField: stringOpt(parsed.options["id-field"]),
|
|
1740
|
+
key: stringOpt(parsed.options.key),
|
|
1741
|
+
generateIds: parsed.options["generate-ids"] === true,
|
|
1742
|
+
dryRun: parsed.options["dry-run"] === true,
|
|
1743
|
+
yes: parsed.options.yes === true,
|
|
1744
|
+
token: stringOpt(parsed.options.token),
|
|
1745
|
+
email: stringOpt(parsed.options.email),
|
|
1746
|
+
json: parsed.options.json === true,
|
|
1747
|
+
fetch: dependencies.fetch,
|
|
1748
|
+
stdout: dependencies.stdout
|
|
1749
|
+
});
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1486
1752
|
if (sub !== "archive" && sub !== "restore") {
|
|
1487
1753
|
throw new Error(
|
|
1488
|
-
`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.)`
|
|
1754
|
+
`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.)`
|
|
1489
1755
|
);
|
|
1490
1756
|
}
|
|
1491
1757
|
assertArgs(parsed, ["config", "token", "email", "yes", "json"], 2);
|
|
@@ -1527,7 +1793,7 @@ var CAPABILITIES = {
|
|
|
1527
1793
|
human: [
|
|
1528
1794
|
"provide the existing odla account email, then sign in and explicitly review/approve the exact device code",
|
|
1529
1795
|
"review mirrored calendar/attendee disclosure and complete the server-issued Google consent flow",
|
|
1530
|
-
"consent to
|
|
1796
|
+
"consent to changes against an app's live database with --yes",
|
|
1531
1797
|
"request destructive credential rotation explicitly",
|
|
1532
1798
|
"review application semantics, security findings, releases, and merges",
|
|
1533
1799
|
"approve redacted-source disclosure before hosted security reasoning",
|
|
@@ -1992,7 +2258,7 @@ function printStatus(status, json, out) {
|
|
|
1992
2258
|
}
|
|
1993
2259
|
|
|
1994
2260
|
// src/code-connect.ts
|
|
1995
|
-
var
|
|
2261
|
+
var import_node_fs8 = require("fs");
|
|
1996
2262
|
var import_node_os2 = require("os");
|
|
1997
2263
|
var import_node_path5 = require("path");
|
|
1998
2264
|
|
|
@@ -4879,9 +5145,9 @@ var CodePiRuntimeEngine = class {
|
|
|
4879
5145
|
};
|
|
4880
5146
|
|
|
4881
5147
|
// src/help.ts
|
|
4882
|
-
var
|
|
5148
|
+
var import_node_fs7 = require("fs");
|
|
4883
5149
|
function cliVersion() {
|
|
4884
|
-
const pkg = JSON.parse((0,
|
|
5150
|
+
const pkg = JSON.parse((0, import_node_fs7.readFileSync)(new URL("../package.json", importMetaUrl), "utf8"));
|
|
4885
5151
|
return pkg.version ?? "unknown";
|
|
4886
5152
|
}
|
|
4887
5153
|
function printHelp() {
|
|
@@ -4898,6 +5164,10 @@ Usage:
|
|
|
4898
5164
|
odla-ai app archive [--config odla.config.mjs] [--email <odla-account>] [--json] --yes
|
|
4899
5165
|
odla-ai app restore [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
4900
5166
|
odla-ai app export [--env dev] [--fresh] [--out <file>] [--email <odla-account>] [--json]
|
|
5167
|
+
odla-ai app import <file|-> [--env dev] [--ns <namespace>] [--id-field <f>|--key <attr>|--generate-ids] [--dry-run] [--json] --yes
|
|
5168
|
+
odla-ai app refresh-sandbox [--include-identity] [--include-files] [--dry-run] [--json] --yes
|
|
5169
|
+
odla-ai app go-live [--include-identity] [--include-files] [--dry-run] [--json] --yes
|
|
5170
|
+
odla-ai app promote [--dry-run] [--json] --yes
|
|
4901
5171
|
odla-ai app owners list [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
4902
5172
|
odla-ai app owners add <email> [--email <odla-account>] [--json]
|
|
4903
5173
|
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
@@ -4934,17 +5204,29 @@ Commands:
|
|
|
4934
5204
|
init Create a generic odla.config.mjs plus starter schema/rules files.
|
|
4935
5205
|
doctor Validate and summarize the project config without network calls.
|
|
4936
5206
|
calendar Inspect, connect, or disconnect the live Google booking connection.
|
|
4937
|
-
app Archive (suspend, data retained), restore, export, or
|
|
4938
|
-
co-owners of the app. Archiving takes every
|
|
4939
|
-
plane down until restored; permanent deletion
|
|
4940
|
-
requires a signed-in owner in Studio, and no
|
|
4941
|
-
credential can purge. "app export" downloads a
|
|
4942
|
-
gzipped-JSONL snapshot of one
|
|
4943
|
-
|
|
5207
|
+
app Archive (suspend, data retained), restore, export, import, or
|
|
5208
|
+
manage the co-owners of the app. Archiving takes every
|
|
5209
|
+
environment's data plane down until restored; permanent deletion
|
|
5210
|
+
has NO CLI \u2014 it requires a signed-in owner in Studio, and no
|
|
5211
|
+
machine or agent credential can purge. "app export" downloads a
|
|
5212
|
+
portable gzipped-JSONL snapshot of one database (--fresh takes a
|
|
5213
|
+
new snapshot first) \u2014 your data is always yours to take.
|
|
5214
|
+
"app import" upserts rows back in from JSON, JSONL, or a
|
|
5215
|
+
{namespace: rows} map (the shape a query returns); it writes only
|
|
5216
|
+
with --yes, so the bare command is a dry run, and it refuses to
|
|
5217
|
+
guess an id mode that would duplicate rows on a re-run.
|
|
5218
|
+
"app refresh-sandbox" replaces the sandbox with a copy of live
|
|
5219
|
+
(the routine dev loop); "app go-live" copies the sandbox into an
|
|
5220
|
+
EMPTY live database, once, at launch; "app promote" pushes only
|
|
5221
|
+
schema, rules and gates up afterwards, leaving live's rows alone.
|
|
5222
|
+
Each prints its plan and writes nothing without --yes, so the
|
|
5223
|
+
bare command is the dry run. Clerk config, triggers, allowlists,
|
|
5224
|
+
secrets and API keys never travel; identity rows stay behind
|
|
5225
|
+
unless --include-identity.
|
|
4944
5226
|
"app owners add <email>" grants a signed-up odla member the SAME
|
|
4945
5227
|
full access as you; they then run their own "provision" to mint
|
|
4946
|
-
their own credentials for the shared db (
|
|
4947
|
-
secret is ever copied between people.
|
|
5228
|
+
their own credentials for the shared db (the live one included)
|
|
5229
|
+
\u2014 no secret is ever copied between people.
|
|
4948
5230
|
capabilities Show what the CLI automates vs agent edits and human checkpoints.
|
|
4949
5231
|
code Enroll this Mac/Linux host for the current Studio-connected repository and run Pi.
|
|
4950
5232
|
admin Manage platform-funded AI routing/credentials/usage with narrow device grants.
|
|
@@ -4959,8 +5241,11 @@ Commands:
|
|
|
4959
5241
|
version Print the CLI version.
|
|
4960
5242
|
|
|
4961
5243
|
Safety:
|
|
4962
|
-
|
|
4963
|
-
|
|
5244
|
+
Every app has two databases on odla.ai: a sandbox (env "dev", tenant
|
|
5245
|
+
<appId>--dev) and a live one (env "prod", tenant <appId>). Both are served by
|
|
5246
|
+
production odla.ai \u2014 there is no separate odla to point at. New projects get
|
|
5247
|
+
the sandbox only. Add "prod" explicitly to odla.config.mjs and pass --yes to
|
|
5248
|
+
provision the live database; use --dry-run first to inspect the resolved plan.
|
|
4964
5249
|
Provision caches the approved developer token and service credentials under
|
|
4965
5250
|
.odla/ with mode 0600, and init adds those paths to .gitignore. Secret push
|
|
4966
5251
|
preflights Wrangler before any shown-once issuance or destructive rotation.
|
|
@@ -5375,7 +5660,7 @@ async function buildEmbeddedPiImage(engine, image, run) {
|
|
|
5375
5660
|
async function codeConnect(options) {
|
|
5376
5661
|
const cwd = options.cwd ?? process.cwd();
|
|
5377
5662
|
const configPath = (0, import_node_path5.resolve)(cwd, options.configPath);
|
|
5378
|
-
const cfg = (0,
|
|
5663
|
+
const cfg = (0, import_node_fs8.existsSync)(configPath) ? await loadProjectConfig(configPath) : null;
|
|
5379
5664
|
const requestedAppId = options.appId?.trim();
|
|
5380
5665
|
if (requestedAppId && !/^[a-z0-9][a-z0-9-]{1,62}$/.test(requestedAppId)) {
|
|
5381
5666
|
throw new Error("--app-id must be a valid odla app id");
|
|
@@ -5385,9 +5670,9 @@ async function codeConnect(options) {
|
|
|
5385
5670
|
}
|
|
5386
5671
|
const appId = requestedAppId ?? cfg?.app.id;
|
|
5387
5672
|
const platform = (options.platform ?? cfg?.platformUrl ?? process.env.ODLA_PLATFORM_URL ?? "https://odla.ai").replace(/\/+$/, "");
|
|
5388
|
-
const appEnv = options.env ?? (cfg ? cfg.envs.includes("dev") ? "dev" : cfg.envs[0] :
|
|
5673
|
+
const appEnv = options.env ?? (cfg ? cfg.envs.includes("dev") ? "dev" : cfg.envs[0] : void 0);
|
|
5389
5674
|
if (appEnv !== "dev" && appEnv !== "prod" || cfg && !cfg.envs.includes(appEnv)) {
|
|
5390
|
-
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`);
|
|
5675
|
+
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}`);
|
|
5391
5676
|
}
|
|
5392
5677
|
if (process.platform !== "darwin" && process.platform !== "linux") {
|
|
5393
5678
|
throw new Error("odla Code hosts require macOS or Linux");
|
|
@@ -5594,12 +5879,12 @@ async function codeCommand(parsed, dependencies) {
|
|
|
5594
5879
|
|
|
5595
5880
|
// src/doctor-checks.ts
|
|
5596
5881
|
var import_node_child_process6 = require("child_process");
|
|
5597
|
-
var
|
|
5882
|
+
var import_node_fs10 = require("fs");
|
|
5598
5883
|
var import_node_path7 = require("path");
|
|
5599
5884
|
|
|
5600
5885
|
// src/wrangler.ts
|
|
5601
5886
|
var import_node_child_process5 = require("child_process");
|
|
5602
|
-
var
|
|
5887
|
+
var import_node_fs9 = require("fs");
|
|
5603
5888
|
var import_node_path6 = require("path");
|
|
5604
5889
|
var defaultRunner = (cmd, args, opts) => new Promise((resolvePromise, reject) => {
|
|
5605
5890
|
const child = (0, import_node_child_process5.spawn)(cmd, args, { cwd: opts?.cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -5615,14 +5900,14 @@ var WRANGLER_CONFIG_FILES = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"]
|
|
|
5615
5900
|
function findWranglerConfig(rootDir) {
|
|
5616
5901
|
for (const name of WRANGLER_CONFIG_FILES) {
|
|
5617
5902
|
const path = (0, import_node_path6.join)(rootDir, name);
|
|
5618
|
-
if ((0,
|
|
5903
|
+
if ((0, import_node_fs9.existsSync)(path)) return path;
|
|
5619
5904
|
}
|
|
5620
5905
|
return null;
|
|
5621
5906
|
}
|
|
5622
5907
|
function readWranglerConfig(path) {
|
|
5623
5908
|
if (path.endsWith(".toml")) return null;
|
|
5624
5909
|
try {
|
|
5625
|
-
return JSON.parse(stripJsonComments((0,
|
|
5910
|
+
return JSON.parse(stripJsonComments((0, import_node_fs9.readFileSync)(path, "utf8")));
|
|
5626
5911
|
} catch {
|
|
5627
5912
|
return null;
|
|
5628
5913
|
}
|
|
@@ -5723,7 +6008,7 @@ function wranglerWarnings(rootDir) {
|
|
|
5723
6008
|
const dir = (0, import_node_path7.resolve)(rootDir, assets.directory);
|
|
5724
6009
|
if (dir === (0, import_node_path7.resolve)(rootDir)) {
|
|
5725
6010
|
warnings.push(`${label}assets.directory is the project root \u2014 point it at a dedicated build dir (wrangler dev fails with "spawn EBADF")`);
|
|
5726
|
-
} else if ((0,
|
|
6011
|
+
} else if ((0, import_node_fs10.existsSync)((0, import_node_path7.join)(dir, "node_modules"))) {
|
|
5727
6012
|
warnings.push(`${label}assets.directory contains node_modules \u2014 wrangler dev's watcher will exhaust file descriptors`);
|
|
5728
6013
|
}
|
|
5729
6014
|
}
|
|
@@ -5759,12 +6044,12 @@ function o11yProjectWarnings(rootDir) {
|
|
|
5759
6044
|
return warnings;
|
|
5760
6045
|
}
|
|
5761
6046
|
const main = typeof config.main === "string" ? (0, import_node_path7.resolve)(rootDir, config.main) : null;
|
|
5762
|
-
if (!main || !(0,
|
|
6047
|
+
if (!main || !(0, import_node_fs10.existsSync)(main)) {
|
|
5763
6048
|
warnings.push("cannot verify o11y Worker instrumentation \u2014 wrangler main is missing or unreadable");
|
|
5764
6049
|
} else {
|
|
5765
6050
|
let source = "";
|
|
5766
6051
|
try {
|
|
5767
|
-
source = (0,
|
|
6052
|
+
source = (0, import_node_fs10.readFileSync)(main, "utf8");
|
|
5768
6053
|
} catch {
|
|
5769
6054
|
}
|
|
5770
6055
|
if (!/\bwithObservability\b/.test(source)) {
|
|
@@ -5788,7 +6073,7 @@ function calendarProjectWarnings(rootDir) {
|
|
|
5788
6073
|
}
|
|
5789
6074
|
function readPackageJson(rootDir) {
|
|
5790
6075
|
try {
|
|
5791
|
-
return JSON.parse((0,
|
|
6076
|
+
return JSON.parse((0, import_node_fs10.readFileSync)((0, import_node_path7.join)(rootDir, "package.json"), "utf8"));
|
|
5792
6077
|
} catch {
|
|
5793
6078
|
return null;
|
|
5794
6079
|
}
|
|
@@ -5989,13 +6274,13 @@ function harnessOption(value, flag) {
|
|
|
5989
6274
|
}
|
|
5990
6275
|
|
|
5991
6276
|
// src/init.ts
|
|
5992
|
-
var
|
|
6277
|
+
var import_node_fs11 = require("fs");
|
|
5993
6278
|
var import_node_path8 = require("path");
|
|
5994
6279
|
function initProject(options) {
|
|
5995
6280
|
const out = options.stdout ?? console;
|
|
5996
6281
|
const rootDir = (0, import_node_path8.resolve)(options.rootDir ?? process.cwd());
|
|
5997
6282
|
const configPath = (0, import_node_path8.resolve)(rootDir, options.configPath ?? "odla.config.mjs");
|
|
5998
|
-
if ((0,
|
|
6283
|
+
if ((0, import_node_fs11.existsSync)(configPath) && !options.force) {
|
|
5999
6284
|
throw new Error(`${configPath} already exists. Pass --force to overwrite.`);
|
|
6000
6285
|
}
|
|
6001
6286
|
if (!/^[a-z0-9][a-z0-9-]*$/.test(options.appId)) {
|
|
@@ -6005,10 +6290,10 @@ function initProject(options) {
|
|
|
6005
6290
|
const services = options.services?.length ? options.services : ["db", "ai"];
|
|
6006
6291
|
if (services.includes("calendar") && !services.includes("db")) throw new Error("--services calendar requires db");
|
|
6007
6292
|
const aiProvider = options.aiProvider ?? "anthropic";
|
|
6008
|
-
(0,
|
|
6009
|
-
(0,
|
|
6010
|
-
(0,
|
|
6011
|
-
(0,
|
|
6293
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path8.dirname)(configPath), { recursive: true });
|
|
6294
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path8.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
6295
|
+
(0, import_node_fs11.mkdirSync)((0, import_node_path8.resolve)(rootDir, ".odla"), { recursive: true });
|
|
6296
|
+
(0, import_node_fs11.writeFileSync)(configPath, configTemplate({ appId: options.appId, name: options.name, envs, services, aiProvider }));
|
|
6012
6297
|
writeIfMissing((0, import_node_path8.resolve)(rootDir, "src/odla/schema.mjs"), schemaTemplate());
|
|
6013
6298
|
writeIfMissing((0, import_node_path8.resolve)(rootDir, "src/odla/rules.mjs"), rulesTemplate());
|
|
6014
6299
|
ensureGitignore(rootDir);
|
|
@@ -6017,8 +6302,8 @@ function initProject(options) {
|
|
|
6017
6302
|
out.log("updated .gitignore for local odla credentials");
|
|
6018
6303
|
}
|
|
6019
6304
|
function writeIfMissing(path, text) {
|
|
6020
|
-
if ((0,
|
|
6021
|
-
(0,
|
|
6305
|
+
if ((0, import_node_fs11.existsSync)(path)) return;
|
|
6306
|
+
(0, import_node_fs11.writeFileSync)(path, text);
|
|
6022
6307
|
}
|
|
6023
6308
|
function configTemplate(input) {
|
|
6024
6309
|
const calendar = input.services.includes("calendar") ? ` calendar: {
|
|
@@ -6119,7 +6404,7 @@ function relativeDisplay(path, rootDir) {
|
|
|
6119
6404
|
}
|
|
6120
6405
|
|
|
6121
6406
|
// src/provision.ts
|
|
6122
|
-
var
|
|
6407
|
+
var import_apps4 = require("@odla-ai/apps");
|
|
6123
6408
|
var import_ai2 = require("@odla-ai/ai");
|
|
6124
6409
|
var import_node_process7 = __toESM(require("process"), 1);
|
|
6125
6410
|
|
|
@@ -6176,9 +6461,9 @@ async function responseText(res) {
|
|
|
6176
6461
|
}
|
|
6177
6462
|
|
|
6178
6463
|
// src/provision-credentials.ts
|
|
6179
|
-
var
|
|
6464
|
+
var import_apps2 = require("@odla-ai/apps");
|
|
6180
6465
|
async function provisionEnvCredentials(opts) {
|
|
6181
|
-
const tenantId = (0,
|
|
6466
|
+
const tenantId = (0, import_apps2.tenantIdFor)(opts.cfg.app.id, opts.env);
|
|
6182
6467
|
const prior = opts.credentials?.envs[opts.env];
|
|
6183
6468
|
let credentials = opts.credentials;
|
|
6184
6469
|
let dbKey = opts.cfg.services.includes("db") && !opts.rotateDb ? prior?.dbKey : void 0;
|
|
@@ -6272,7 +6557,7 @@ async function safeText3(res) {
|
|
|
6272
6557
|
|
|
6273
6558
|
// src/provision-helpers.ts
|
|
6274
6559
|
var import_ai = require("@odla-ai/ai");
|
|
6275
|
-
var
|
|
6560
|
+
var import_apps3 = require("@odla-ai/apps");
|
|
6276
6561
|
function serviceRank(service) {
|
|
6277
6562
|
if (service === "db") return 0;
|
|
6278
6563
|
if (service === "calendar") return 2;
|
|
@@ -6283,7 +6568,7 @@ function defaultSecretName(provider) {
|
|
|
6283
6568
|
return names[provider] ?? `${provider}_api_key`;
|
|
6284
6569
|
}
|
|
6285
6570
|
async function assertTenantAdminAccess(doFetch, cfg, env, token) {
|
|
6286
|
-
const tenantId = (0,
|
|
6571
|
+
const tenantId = (0, import_apps3.tenantIdFor)(cfg.app.id, env);
|
|
6287
6572
|
const res = await doFetch(`${cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/entitlements`, {
|
|
6288
6573
|
headers: { authorization: `Bearer ${token}` }
|
|
6289
6574
|
});
|
|
@@ -6426,7 +6711,7 @@ async function provision(options) {
|
|
|
6426
6711
|
const productionEnvs = plan.envs.filter((env) => env === "prod" || env === "production");
|
|
6427
6712
|
if (!options.dryRun && productionEnvs.length > 0 && !options.yes) {
|
|
6428
6713
|
throw new Error(
|
|
6429
|
-
`refusing to provision
|
|
6714
|
+
`refusing to provision the live database (env ${productionEnvs.join(", ")}) without --yes; run --dry-run first`
|
|
6430
6715
|
);
|
|
6431
6716
|
}
|
|
6432
6717
|
const database = await resolveDatabaseConfig(cfg);
|
|
@@ -6474,7 +6759,7 @@ async function provision(options) {
|
|
|
6474
6759
|
}
|
|
6475
6760
|
const doFetch = options.fetch ?? fetch;
|
|
6476
6761
|
const token = await getDeveloperToken(cfg, options, doFetch, out);
|
|
6477
|
-
const apps = (0,
|
|
6762
|
+
const apps = (0, import_apps4.createAppsClient)({ endpoint: cfg.platformUrl, token, fetcher: { fetch: doFetch } });
|
|
6478
6763
|
const existing = await readRegistryApp(cfg, token, doFetch);
|
|
6479
6764
|
if (existing) {
|
|
6480
6765
|
out.log(`app: ${cfg.app.id} already exists`);
|
|
@@ -6518,7 +6803,7 @@ async function provision(options) {
|
|
|
6518
6803
|
}
|
|
6519
6804
|
}
|
|
6520
6805
|
for (const env of cfg.envs) {
|
|
6521
|
-
const tenantId = (0,
|
|
6806
|
+
const tenantId = (0, import_apps4.tenantIdFor)(cfg.app.id, env);
|
|
6522
6807
|
credentials = await provisionEnvCredentials({
|
|
6523
6808
|
cfg,
|
|
6524
6809
|
env,
|
|
@@ -6604,7 +6889,7 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
|
|
|
6604
6889
|
|
|
6605
6890
|
// src/secrets-set.ts
|
|
6606
6891
|
var import_ai3 = require("@odla-ai/ai");
|
|
6607
|
-
var
|
|
6892
|
+
var import_apps5 = require("@odla-ai/apps");
|
|
6608
6893
|
var PROD_ENV_NAMES2 = /* @__PURE__ */ new Set(["prod", "production"]);
|
|
6609
6894
|
async function secretsSet(options) {
|
|
6610
6895
|
const name = (options.name ?? "").trim();
|
|
@@ -6656,7 +6941,7 @@ async function resolveVaultWrite(options) {
|
|
|
6656
6941
|
throw new Error(`refusing to store a secret for "${options.env}" without --yes`);
|
|
6657
6942
|
}
|
|
6658
6943
|
const value = await secretInputValue(options, "secret");
|
|
6659
|
-
return { cfg, tenantId: (0,
|
|
6944
|
+
return { cfg, tenantId: (0, import_apps5.tenantIdFor)(cfg.app.id, options.env), value, doFetch, out };
|
|
6660
6945
|
}
|
|
6661
6946
|
|
|
6662
6947
|
// src/security-command-context.ts
|
|
@@ -7359,7 +7644,7 @@ async function securityStatus(parsed, dependencies) {
|
|
|
7359
7644
|
}
|
|
7360
7645
|
|
|
7361
7646
|
// src/skill.ts
|
|
7362
|
-
var
|
|
7647
|
+
var import_node_fs12 = require("fs");
|
|
7363
7648
|
var import_node_os3 = require("os");
|
|
7364
7649
|
var import_node_path10 = require("path");
|
|
7365
7650
|
var import_node_url3 = require("url");
|
|
@@ -7437,7 +7722,7 @@ function installSkill(options = {}) {
|
|
|
7437
7722
|
plans.set(target, { target, content, boundary, managedMerge });
|
|
7438
7723
|
};
|
|
7439
7724
|
const planSkillTree = (targetDir2, boundary = root) => {
|
|
7440
|
-
for (const rel of files) plan((0, import_node_path10.join)(targetDir2, rel), (0,
|
|
7725
|
+
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);
|
|
7441
7726
|
};
|
|
7442
7727
|
let targetDir;
|
|
7443
7728
|
if (options.global) {
|
|
@@ -7457,7 +7742,7 @@ function installSkill(options = {}) {
|
|
|
7457
7742
|
for (const harness of harnesses) rememberTarget(harness, sharedRoot);
|
|
7458
7743
|
if (harnesses.includes("claude")) {
|
|
7459
7744
|
for (const skill of skillNames(files)) {
|
|
7460
|
-
const canonical = (0,
|
|
7745
|
+
const canonical = (0, import_node_fs12.readFileSync)((0, import_node_path10.join)(sourceDir, skill, "SKILL.md"), "utf8");
|
|
7461
7746
|
plan((0, import_node_path10.join)(claudeRoot, skill, "SKILL.md"), claudeAdapter(skill, canonical));
|
|
7462
7747
|
}
|
|
7463
7748
|
rememberTarget("claude", claudeRoot);
|
|
@@ -7492,11 +7777,11 @@ function installSkill(options = {}) {
|
|
|
7492
7777
|
conflicts.push(`${file.target} (redirected by symbolic link ${symlink})`);
|
|
7493
7778
|
continue;
|
|
7494
7779
|
}
|
|
7495
|
-
if (!(0,
|
|
7780
|
+
if (!(0, import_node_fs12.existsSync)(file.target)) {
|
|
7496
7781
|
writtenPaths.add(file.target);
|
|
7497
7782
|
continue;
|
|
7498
7783
|
}
|
|
7499
|
-
const current = (0,
|
|
7784
|
+
const current = (0, import_node_fs12.readFileSync)(file.target, "utf8");
|
|
7500
7785
|
if (current === file.content) {
|
|
7501
7786
|
unchangedPaths.add(file.target);
|
|
7502
7787
|
} else if (file.managedMerge || options.force) {
|
|
@@ -7513,9 +7798,9 @@ ${conflicts.map((f) => ` - ${f}`).join("\n")}`
|
|
|
7513
7798
|
);
|
|
7514
7799
|
}
|
|
7515
7800
|
for (const file of plans.values()) {
|
|
7516
|
-
if (!(0,
|
|
7517
|
-
(0,
|
|
7518
|
-
(0,
|
|
7801
|
+
if (!(0, import_node_fs12.existsSync)(file.target) || (0, import_node_fs12.readFileSync)(file.target, "utf8") !== file.content) {
|
|
7802
|
+
(0, import_node_fs12.mkdirSync)((0, import_node_path10.dirname)(file.target), { recursive: true });
|
|
7803
|
+
(0, import_node_fs12.writeFileSync)(file.target, file.content);
|
|
7519
7804
|
}
|
|
7520
7805
|
}
|
|
7521
7806
|
const skills = skillNames(files);
|
|
@@ -7556,9 +7841,9 @@ function normalizeHarnesses(values, global) {
|
|
|
7556
7841
|
function managedFileContent(path, block, force, boundary) {
|
|
7557
7842
|
const symlink = symlinkedComponent(boundary, path);
|
|
7558
7843
|
if (symlink) throw new Error(`refusing to manage ${path}: symbolic link component ${symlink}`);
|
|
7559
|
-
if (!(0,
|
|
7844
|
+
if (!(0, import_node_fs12.existsSync)(path)) return `${block}
|
|
7560
7845
|
`;
|
|
7561
|
-
const current = (0,
|
|
7846
|
+
const current = (0, import_node_fs12.readFileSync)(path, "utf8");
|
|
7562
7847
|
const start = "<!-- odla-ai agent setup:start -->";
|
|
7563
7848
|
const end = "<!-- odla-ai agent setup:end -->";
|
|
7564
7849
|
const startAt = current.indexOf(start);
|
|
@@ -7587,7 +7872,7 @@ function symlinkedComponent(boundary, target) {
|
|
|
7587
7872
|
for (const part of rel.split(import_node_path10.sep).filter(Boolean)) {
|
|
7588
7873
|
current = (0, import_node_path10.join)(current, part);
|
|
7589
7874
|
try {
|
|
7590
|
-
if ((0,
|
|
7875
|
+
if ((0, import_node_fs12.lstatSync)(current).isSymbolicLink()) return current;
|
|
7591
7876
|
} catch (error) {
|
|
7592
7877
|
if (error.code !== "ENOENT") throw error;
|
|
7593
7878
|
}
|
|
@@ -7598,10 +7883,10 @@ function skillNames(files) {
|
|
|
7598
7883
|
return [...new Set(files.filter((file) => /(^|[\\/])SKILL\.md$/.test(file)).map((file) => file.split(/[\\/]/)[0]))].sort();
|
|
7599
7884
|
}
|
|
7600
7885
|
function listFiles(dir) {
|
|
7601
|
-
if (!(0,
|
|
7886
|
+
if (!(0, import_node_fs12.existsSync)(dir)) return [];
|
|
7602
7887
|
const results = [];
|
|
7603
7888
|
const walk = (current) => {
|
|
7604
|
-
for (const entry of (0,
|
|
7889
|
+
for (const entry of (0, import_node_fs12.readdirSync)(current, { withFileTypes: true })) {
|
|
7605
7890
|
const path = (0, import_node_path10.join)(current, entry.name);
|
|
7606
7891
|
if (entry.isDirectory()) walk(path);
|
|
7607
7892
|
else results.push((0, import_node_path10.relative)(dir, path));
|