@pebblehouse/odin-cli 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +443 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ All commands output JSON to stdout. Errors go to stderr with non-zero exit codes
|
|
|
82
82
|
|----------|---------|-------------|
|
|
83
83
|
| `ODIN_API_URL` | `https://www.odin.mu` | API base URL |
|
|
84
84
|
| `ODIN_SUPABASE_URL` | — | Supabase URL (for token refresh) |
|
|
85
|
-
| `
|
|
85
|
+
| `ODIN_SUPABASE_PUBLISHABLE_KEY` | — | Supabase publishable key (for token refresh) |
|
|
86
86
|
|
|
87
87
|
## License
|
|
88
88
|
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command21 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/auth/login.ts
|
|
7
7
|
import { createServer } from "http";
|
|
@@ -37,7 +37,7 @@ function clearCredentials() {
|
|
|
37
37
|
// src/auth/login.ts
|
|
38
38
|
var API_URL = process.env.ODIN_API_URL ?? "https://www.odin.mu";
|
|
39
39
|
async function login() {
|
|
40
|
-
return new Promise((
|
|
40
|
+
return new Promise((resolve3, reject) => {
|
|
41
41
|
const server = createServer(async (req, res) => {
|
|
42
42
|
if (req.method === "POST" && req.url?.startsWith("/callback")) {
|
|
43
43
|
let body = "";
|
|
@@ -70,7 +70,7 @@ async function login() {
|
|
|
70
70
|
</div>
|
|
71
71
|
</body></html>`);
|
|
72
72
|
server.close();
|
|
73
|
-
|
|
73
|
+
resolve3();
|
|
74
74
|
} else {
|
|
75
75
|
res.writeHead(404);
|
|
76
76
|
res.end();
|
|
@@ -105,12 +105,12 @@ import { createClient } from "@supabase/supabase-js";
|
|
|
105
105
|
import { existsSync as existsSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, readFileSync as readFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
106
106
|
import { dirname } from "path";
|
|
107
107
|
var SUPABASE_URL = process.env.ODIN_SUPABASE_URL ?? "https://vdiwtiiksdyhlibqrngw.supabase.co";
|
|
108
|
-
var
|
|
108
|
+
var SUPABASE_PUBLISHABLE_KEY = process.env.ODIN_SUPABASE_PUBLISHABLE_KEY ?? "sb_publishable_qTgDHReuOlNcsxm_r7PIrg_K-EV3F7a";
|
|
109
109
|
var REFRESH_THRESHOLD_SECONDS = 900;
|
|
110
110
|
var LOCK_PATH = CREDENTIALS_PATH + ".lock";
|
|
111
111
|
var LOCK_STALE_MS = 1e4;
|
|
112
112
|
function createNodeClient() {
|
|
113
|
-
return createClient(SUPABASE_URL,
|
|
113
|
+
return createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
|
|
114
114
|
auth: {
|
|
115
115
|
autoRefreshToken: false,
|
|
116
116
|
persistSession: false
|
|
@@ -1274,12 +1274,446 @@ apiKeysCmd.command("delete <slug> <id>").description("Permanently delete an API
|
|
|
1274
1274
|
if (res.error) process.exit(1);
|
|
1275
1275
|
});
|
|
1276
1276
|
|
|
1277
|
+
// src/commands/pebble/index.ts
|
|
1278
|
+
import { Command as Command20 } from "commander";
|
|
1279
|
+
|
|
1280
|
+
// src/commands/pebble/provisioner.ts
|
|
1281
|
+
async function runProvisioner(ctx, steps) {
|
|
1282
|
+
const report = { steps: [], captured: ctx.captured };
|
|
1283
|
+
for (const step of steps) {
|
|
1284
|
+
let result;
|
|
1285
|
+
const checked = await step.check(ctx);
|
|
1286
|
+
if (checked && checked.status === "satisfied") result = checked;
|
|
1287
|
+
else if (checked && checked.status === "blocked") result = checked;
|
|
1288
|
+
else result = await step.apply(ctx);
|
|
1289
|
+
if (result.data) {
|
|
1290
|
+
Object.assign(ctx.captured, result.data);
|
|
1291
|
+
}
|
|
1292
|
+
report.steps.push({
|
|
1293
|
+
id: step.id,
|
|
1294
|
+
label: step.label,
|
|
1295
|
+
status: result.status,
|
|
1296
|
+
detail: result.detail
|
|
1297
|
+
});
|
|
1298
|
+
if (result.status === "blocked") return { report, halted: true };
|
|
1299
|
+
if (result.status === "failed") return { report, halted: true };
|
|
1300
|
+
}
|
|
1301
|
+
return { report, halted: false };
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// src/commands/pebble/exec.ts
|
|
1305
|
+
import { execFile } from "child_process";
|
|
1306
|
+
function run(cmd, args, opts = {}) {
|
|
1307
|
+
return new Promise((resolve3) => {
|
|
1308
|
+
execFile(cmd, args, { cwd: opts.cwd, encoding: "utf-8" }, (err, stdout, stderr) => {
|
|
1309
|
+
const code = err && typeof err.code === "number" ? err.code : err ? 1 : 0;
|
|
1310
|
+
resolve3({ ok: code === 0, stdout: stdout ?? "", stderr: stderr ?? "", code });
|
|
1311
|
+
});
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/commands/pebble/steps/doppler.ts
|
|
1316
|
+
var dopplerStep = {
|
|
1317
|
+
id: "doppler",
|
|
1318
|
+
label: "Doppler project + dev/prd configs",
|
|
1319
|
+
async check(ctx) {
|
|
1320
|
+
const cli = await run("doppler", ["--version"]);
|
|
1321
|
+
if (!cli.ok) {
|
|
1322
|
+
return { status: "blocked", detail: "Doppler CLI not found. Install + run `doppler login`." };
|
|
1323
|
+
}
|
|
1324
|
+
const got = await run("doppler", ["projects", "get", ctx.slug]);
|
|
1325
|
+
return got.ok ? { status: "satisfied", detail: `Project ${ctx.slug} exists` } : null;
|
|
1326
|
+
},
|
|
1327
|
+
async apply(ctx) {
|
|
1328
|
+
if (ctx.dryRun) {
|
|
1329
|
+
return {
|
|
1330
|
+
status: "applied",
|
|
1331
|
+
detail: `[dry-run] would create Doppler project ${ctx.slug} + dev/prd configs`
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
const c = await run("doppler", ["projects", "create", ctx.slug]);
|
|
1335
|
+
if (!c.ok) return { status: "failed", detail: `doppler projects create failed: ${c.stderr}` };
|
|
1336
|
+
await run("doppler", ["configs", "create", "dev", "-p", ctx.slug]);
|
|
1337
|
+
await run("doppler", ["configs", "create", "prd", "-p", ctx.slug]);
|
|
1338
|
+
return { status: "applied", detail: `Created Doppler project ${ctx.slug} + dev/prd configs` };
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
// src/commands/pebble/steps/github.ts
|
|
1343
|
+
import { spawn } from "child_process";
|
|
1344
|
+
import { resolve } from "path";
|
|
1345
|
+
var ORG = "pebblehouse";
|
|
1346
|
+
var TEMPLATE = "pebblehouse/pebble-template";
|
|
1347
|
+
var githubRepoStep = {
|
|
1348
|
+
id: "github",
|
|
1349
|
+
label: "GitHub repo from pebble-template",
|
|
1350
|
+
async check(ctx) {
|
|
1351
|
+
const cli = await run("gh", ["--version"]);
|
|
1352
|
+
if (!cli.ok) {
|
|
1353
|
+
return { status: "blocked", detail: "GitHub CLI not found. Install + run `gh auth login`." };
|
|
1354
|
+
}
|
|
1355
|
+
const view = await run("gh", ["repo", "view", `${ORG}/${ctx.slug}`]);
|
|
1356
|
+
if (view.ok) {
|
|
1357
|
+
const repoDir = ctx.repoDir ?? resolve(ctx.repoDir ?? ".", ctx.slug);
|
|
1358
|
+
return {
|
|
1359
|
+
status: "satisfied",
|
|
1360
|
+
detail: `Repo ${ORG}/${ctx.slug} exists`,
|
|
1361
|
+
data: { repoDir }
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
return null;
|
|
1365
|
+
},
|
|
1366
|
+
async apply(ctx) {
|
|
1367
|
+
if (ctx.dryRun) {
|
|
1368
|
+
return {
|
|
1369
|
+
status: "applied",
|
|
1370
|
+
detail: `[dry-run] would create ${ORG}/${ctx.slug} from ${TEMPLATE}`
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
const parent = ctx.repoDir ?? ".";
|
|
1374
|
+
const c = await run(
|
|
1375
|
+
"gh",
|
|
1376
|
+
[
|
|
1377
|
+
"repo",
|
|
1378
|
+
"create",
|
|
1379
|
+
`${ORG}/${ctx.slug}`,
|
|
1380
|
+
"--template",
|
|
1381
|
+
TEMPLATE,
|
|
1382
|
+
"--private",
|
|
1383
|
+
"--clone"
|
|
1384
|
+
],
|
|
1385
|
+
{ cwd: parent }
|
|
1386
|
+
);
|
|
1387
|
+
if (!c.ok) return { status: "failed", detail: `gh repo create failed: ${c.stderr}` };
|
|
1388
|
+
const repoDir = resolve(parent, ctx.slug);
|
|
1389
|
+
return {
|
|
1390
|
+
status: "applied",
|
|
1391
|
+
detail: `Created ${ORG}/${ctx.slug} and cloned to ${repoDir}`,
|
|
1392
|
+
data: { repoDir }
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
var githubSecretStep = {
|
|
1397
|
+
id: "github-secret",
|
|
1398
|
+
label: "GitHub Actions secret DOPPLER_TOKEN_PRD",
|
|
1399
|
+
async check(ctx) {
|
|
1400
|
+
const cli = await run("gh", ["--version"]);
|
|
1401
|
+
if (!cli.ok) {
|
|
1402
|
+
return { status: "blocked", detail: "GitHub CLI not found. Install + run `gh auth login`." };
|
|
1403
|
+
}
|
|
1404
|
+
const list = await run("gh", ["secret", "list", "-R", `${ORG}/${ctx.slug}`]);
|
|
1405
|
+
if (list.ok && /DOPPLER_TOKEN_PRD/.test(list.stdout)) {
|
|
1406
|
+
return { status: "satisfied", detail: "DOPPLER_TOKEN_PRD already set" };
|
|
1407
|
+
}
|
|
1408
|
+
return null;
|
|
1409
|
+
},
|
|
1410
|
+
async apply(ctx) {
|
|
1411
|
+
if (ctx.dryRun) {
|
|
1412
|
+
return {
|
|
1413
|
+
status: "applied",
|
|
1414
|
+
detail: "[dry-run] would mint scoped prd token and set DOPPLER_TOKEN_PRD"
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
const token = await run("doppler", [
|
|
1418
|
+
"configs",
|
|
1419
|
+
"tokens",
|
|
1420
|
+
"create",
|
|
1421
|
+
"ci",
|
|
1422
|
+
"-p",
|
|
1423
|
+
ctx.slug,
|
|
1424
|
+
"-c",
|
|
1425
|
+
"prd",
|
|
1426
|
+
"--plain"
|
|
1427
|
+
]);
|
|
1428
|
+
if (!token.ok) {
|
|
1429
|
+
return { status: "failed", detail: `doppler token mint failed: ${token.stderr}` };
|
|
1430
|
+
}
|
|
1431
|
+
const set = await setSecretViaStdin(
|
|
1432
|
+
["secret", "set", "DOPPLER_TOKEN_PRD", "-R", `${ORG}/${ctx.slug}`],
|
|
1433
|
+
token.stdout.trim()
|
|
1434
|
+
);
|
|
1435
|
+
if (!set.ok) {
|
|
1436
|
+
return { status: "failed", detail: `gh secret set failed: ${set.stderr}` };
|
|
1437
|
+
}
|
|
1438
|
+
return { status: "applied", detail: "Set DOPPLER_TOKEN_PRD (read-only prd token)" };
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
function setSecretViaStdin(args, value) {
|
|
1442
|
+
return new Promise((resolvePromise) => {
|
|
1443
|
+
const child = spawn("gh", args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
1444
|
+
let stderr = "";
|
|
1445
|
+
child.stderr.on("data", (d) => {
|
|
1446
|
+
stderr += d.toString();
|
|
1447
|
+
});
|
|
1448
|
+
child.on("error", (e) => resolvePromise({ ok: false, stderr: e.message }));
|
|
1449
|
+
child.on("close", (code) => resolvePromise({ ok: code === 0, stderr }));
|
|
1450
|
+
child.stdin.write(value);
|
|
1451
|
+
child.stdin.end();
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// src/commands/pebble/steps/vercel.ts
|
|
1456
|
+
var vercelStep = {
|
|
1457
|
+
id: "vercel",
|
|
1458
|
+
label: "Vercel project",
|
|
1459
|
+
async check(ctx) {
|
|
1460
|
+
const cli = await run("vercel", ["--version"]);
|
|
1461
|
+
if (!cli.ok) {
|
|
1462
|
+
return { status: "blocked", detail: "Vercel CLI not found. Install + run `vercel login`." };
|
|
1463
|
+
}
|
|
1464
|
+
const list = await run("vercel", ["project", "ls"]);
|
|
1465
|
+
if (list.ok && new RegExp(`(^|\\s)${ctx.slug}(\\s|$)`, "m").test(list.stdout)) {
|
|
1466
|
+
return { status: "satisfied", detail: `Vercel project ${ctx.slug} exists` };
|
|
1467
|
+
}
|
|
1468
|
+
return null;
|
|
1469
|
+
},
|
|
1470
|
+
async apply(ctx) {
|
|
1471
|
+
if (ctx.dryRun) {
|
|
1472
|
+
return { status: "applied", detail: `[dry-run] would add Vercel project ${ctx.slug}` };
|
|
1473
|
+
}
|
|
1474
|
+
const c = await run("vercel", ["project", "add", ctx.slug]);
|
|
1475
|
+
if (!c.ok) return { status: "failed", detail: `vercel project add failed: ${c.stderr}` };
|
|
1476
|
+
return { status: "applied", detail: `Added Vercel project ${ctx.slug}` };
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
// src/commands/pebble/steps/r2.ts
|
|
1481
|
+
var bucketName = (slug) => `${slug}-tfstate`;
|
|
1482
|
+
var r2Step = {
|
|
1483
|
+
id: "r2",
|
|
1484
|
+
label: "Cloudflare R2 tfstate bucket",
|
|
1485
|
+
async check(ctx) {
|
|
1486
|
+
const cli = await run("wrangler", ["--version"]);
|
|
1487
|
+
if (!cli.ok) {
|
|
1488
|
+
return { status: "blocked", detail: "Wrangler CLI not found. Install + run `wrangler login`." };
|
|
1489
|
+
}
|
|
1490
|
+
const list = await run("wrangler", ["r2", "bucket", "list"]);
|
|
1491
|
+
if (list.ok && list.stdout.includes(bucketName(ctx.slug))) {
|
|
1492
|
+
return { status: "satisfied", detail: `Bucket ${bucketName(ctx.slug)} exists` };
|
|
1493
|
+
}
|
|
1494
|
+
return null;
|
|
1495
|
+
},
|
|
1496
|
+
async apply(ctx) {
|
|
1497
|
+
if (ctx.dryRun) {
|
|
1498
|
+
return {
|
|
1499
|
+
status: "applied",
|
|
1500
|
+
detail: `[dry-run] would create R2 bucket ${bucketName(ctx.slug)} + store S3 creds to Doppler prd`
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
const c = await run("wrangler", ["r2", "bucket", "create", bucketName(ctx.slug)]);
|
|
1504
|
+
if (!c.ok) return { status: "failed", detail: `r2 bucket create failed: ${c.stderr}` };
|
|
1505
|
+
return {
|
|
1506
|
+
status: "applied",
|
|
1507
|
+
detail: `Created R2 bucket ${bucketName(ctx.slug)}`,
|
|
1508
|
+
data: { tfstate_bucket: bucketName(ctx.slug) }
|
|
1509
|
+
};
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
// src/commands/pebble/steps/supabase.ts
|
|
1514
|
+
var supabaseStep = {
|
|
1515
|
+
id: "supabase",
|
|
1516
|
+
label: "Supabase project",
|
|
1517
|
+
async check(ctx) {
|
|
1518
|
+
const cli = await run("supabase", ["--version"]);
|
|
1519
|
+
if (!cli.ok) {
|
|
1520
|
+
return { status: "blocked", detail: "Supabase CLI not found. Install + run `supabase login`." };
|
|
1521
|
+
}
|
|
1522
|
+
const list = await run("supabase", ["projects", "list"]);
|
|
1523
|
+
if (list.ok && new RegExp(`(^|\\s)${ctx.slug}(\\s|$)`, "m").test(list.stdout)) {
|
|
1524
|
+
return { status: "satisfied", detail: `Supabase project ${ctx.slug} exists` };
|
|
1525
|
+
}
|
|
1526
|
+
return null;
|
|
1527
|
+
},
|
|
1528
|
+
async apply(ctx) {
|
|
1529
|
+
if (ctx.dryRun) {
|
|
1530
|
+
return { status: "applied", detail: `[dry-run] would create Supabase project ${ctx.slug}` };
|
|
1531
|
+
}
|
|
1532
|
+
const c = await run("supabase", ["projects", "create", ctx.slug]);
|
|
1533
|
+
if (!c.ok) return { status: "failed", detail: `supabase projects create failed: ${c.stderr}` };
|
|
1534
|
+
const ref = parseRef(c.stdout);
|
|
1535
|
+
return {
|
|
1536
|
+
status: "applied",
|
|
1537
|
+
detail: `Created Supabase project ${ctx.slug}${ref ? ` (${ref})` : ""}`,
|
|
1538
|
+
data: ref ? { supabase_ref: ref } : void 0
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
function parseRef(stdout) {
|
|
1543
|
+
const m = stdout.match(/[a-z]{20}/);
|
|
1544
|
+
return m ? m[0] : void 0;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// src/commands/pebble/steps/resend.ts
|
|
1548
|
+
var resendStep = {
|
|
1549
|
+
id: "resend",
|
|
1550
|
+
label: "Resend sending domain (send-only)",
|
|
1551
|
+
async check(ctx) {
|
|
1552
|
+
const cli = await run("resend", ["--version"]);
|
|
1553
|
+
if (!cli.ok) {
|
|
1554
|
+
return {
|
|
1555
|
+
status: "blocked",
|
|
1556
|
+
detail: "Resend CLI not found / not configured. Set RESEND_API_KEY and install the CLI."
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
const list = await run("resend", ["domains", "list"]);
|
|
1560
|
+
if (list.ok && list.stdout.includes(ctx.slug)) {
|
|
1561
|
+
return { status: "satisfied", detail: `Resend sending domain for ${ctx.slug} exists` };
|
|
1562
|
+
}
|
|
1563
|
+
return null;
|
|
1564
|
+
},
|
|
1565
|
+
async apply(ctx) {
|
|
1566
|
+
if (ctx.dryRun) {
|
|
1567
|
+
return {
|
|
1568
|
+
status: "applied",
|
|
1569
|
+
detail: `[dry-run] would create Resend sending domain for ${ctx.slug} (send-only)`
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
const c = await run("resend", ["domains", "create", ctx.slug]);
|
|
1573
|
+
if (!c.ok) return { status: "failed", detail: `resend domains create failed: ${c.stderr}` };
|
|
1574
|
+
return { status: "applied", detail: `Created Resend sending domain for ${ctx.slug} (send-only)` };
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
|
|
1578
|
+
// src/commands/pebble/steps/stripe.ts
|
|
1579
|
+
var stripeStep = {
|
|
1580
|
+
id: "stripe",
|
|
1581
|
+
label: "Stripe account (conditional)",
|
|
1582
|
+
async check() {
|
|
1583
|
+
return { status: "satisfied", detail: "conditional \u2014 evaluate in init bet" };
|
|
1584
|
+
},
|
|
1585
|
+
async apply() {
|
|
1586
|
+
return { status: "satisfied", detail: "conditional \u2014 evaluate in init bet" };
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
|
|
1590
|
+
// src/commands/pebble/steps/index.ts
|
|
1591
|
+
var CORE_STEPS = [
|
|
1592
|
+
dopplerStep,
|
|
1593
|
+
githubRepoStep,
|
|
1594
|
+
vercelStep,
|
|
1595
|
+
r2Step,
|
|
1596
|
+
supabaseStep,
|
|
1597
|
+
resendStep,
|
|
1598
|
+
githubSecretStep
|
|
1599
|
+
];
|
|
1600
|
+
var STEP_REGISTRY = {
|
|
1601
|
+
doppler: dopplerStep,
|
|
1602
|
+
github: githubRepoStep,
|
|
1603
|
+
vercel: vercelStep,
|
|
1604
|
+
r2: r2Step,
|
|
1605
|
+
supabase: supabaseStep,
|
|
1606
|
+
resend: resendStep,
|
|
1607
|
+
stripe: stripeStep
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
// src/commands/pebble/init-bet.ts
|
|
1611
|
+
function renderInitBetBody(slug, report) {
|
|
1612
|
+
const rows = report.steps.map((s) => `| ${s.id} | ${s.status} | ${s.detail} |`).join("\n");
|
|
1613
|
+
return `# Initialize ${slug}
|
|
1614
|
+
|
|
1615
|
+
## Problem
|
|
1616
|
+
\`${slug}\` is genesis'd from \`pebble-template\` with the full IaC spine (PLACEHOLDER_* values), but its real stack is unknown. \`infra-apply\` is OFF until the spine is tailored to the actual stack.
|
|
1617
|
+
|
|
1618
|
+
## Solution
|
|
1619
|
+
1. Interview the actual stack (which providers/workflows this pebble needs).
|
|
1620
|
+
2. Trim the template's tofu modules + workflow set to match; remove dangling refs in \`main.tf\`/\`variables.tf\`.
|
|
1621
|
+
3. Provision any conditional providers via \`odin pebble provision <provider> ${slug}\`.
|
|
1622
|
+
4. Worktree eval + run the test suite.
|
|
1623
|
+
5. Flip the deploy-ready signal so \`infra-apply\` runs on merge.
|
|
1624
|
+
|
|
1625
|
+
## Constraints
|
|
1626
|
+
- Brownfield-safe; never blow away live resources.
|
|
1627
|
+
- No plaintext secrets or tfstate in git.
|
|
1628
|
+
- Reuse the codified IaC standard (blueprint 36839408 + pebble-template); no parallel layout.
|
|
1629
|
+
|
|
1630
|
+
## Success Looks Like
|
|
1631
|
+
- Clean no-op \`tofu plan\`.
|
|
1632
|
+
- \`infra-apply\` flipped on and green on merge.
|
|
1633
|
+
- Test suite passes.
|
|
1634
|
+
|
|
1635
|
+
### Provisioned day-one
|
|
1636
|
+
| Provider | Status | Detail |
|
|
1637
|
+
|---|---|---|
|
|
1638
|
+
${rows}
|
|
1639
|
+
|
|
1640
|
+
### Conditional providers to evaluate
|
|
1641
|
+
- [ ] stripe (only if this pebble takes payments)
|
|
1642
|
+
- [ ] Resend inbound / email-routing (only if this pebble receives mail)
|
|
1643
|
+
- [ ] any tail provider specific to the stack
|
|
1644
|
+
`;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// src/commands/pebble/new.ts
|
|
1648
|
+
async function runNew(name, opts) {
|
|
1649
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1650
|
+
const created = await apiRequest("/pebbles", {
|
|
1651
|
+
method: "POST",
|
|
1652
|
+
body: { slug, name }
|
|
1653
|
+
});
|
|
1654
|
+
if (created.error && !/already exists/i.test(created.error)) {
|
|
1655
|
+
console.error(created.error);
|
|
1656
|
+
return 1;
|
|
1657
|
+
}
|
|
1658
|
+
const ctx = {
|
|
1659
|
+
slug,
|
|
1660
|
+
name,
|
|
1661
|
+
repoDir: opts.dir ?? null,
|
|
1662
|
+
dryRun: !!opts.dryRun,
|
|
1663
|
+
captured: {}
|
|
1664
|
+
};
|
|
1665
|
+
const { report, halted } = await runProvisioner(ctx, CORE_STEPS);
|
|
1666
|
+
const last = report.steps[report.steps.length - 1];
|
|
1667
|
+
process.stdout.write(JSON.stringify({ slug, report }) + "\n");
|
|
1668
|
+
if (halted && last?.status === "failed") return 1;
|
|
1669
|
+
if (halted && last?.status === "blocked") {
|
|
1670
|
+
process.stderr.write(`
|
|
1671
|
+
Paused: ${last.detail}
|
|
1672
|
+
Re-run \`odin pebble new ${name}\` after acting.
|
|
1673
|
+
`);
|
|
1674
|
+
return 0;
|
|
1675
|
+
}
|
|
1676
|
+
const body = renderInitBetBody(slug, report);
|
|
1677
|
+
const bet = await apiRequest(`/pebbles/${slug}/bets`, {
|
|
1678
|
+
method: "POST",
|
|
1679
|
+
body: { title: `Initialize ${slug}`, source: "odin-cli", content: body, status: "shaped" }
|
|
1680
|
+
});
|
|
1681
|
+
process.stdout.write(JSON.stringify(bet) + "\n");
|
|
1682
|
+
if (opts.run && bet.data?.id) {
|
|
1683
|
+
process.stderr.write(`
|
|
1684
|
+
Hand off: /odin:eng-manager bet:${bet.data.id}
|
|
1685
|
+
`);
|
|
1686
|
+
}
|
|
1687
|
+
return 0;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// src/commands/pebble/index.ts
|
|
1691
|
+
var pebbleCmd = new Command20("pebble").description(
|
|
1692
|
+
"Provision and initialize pebbles (eng-infra, run-once)"
|
|
1693
|
+
);
|
|
1694
|
+
pebbleCmd.command("new <name>").description("Provision a new pebble from pebble-template (day-one)").option("--run", "Hand off to eng-manager on the init bet").option("--dir <path>", "Parent dir to clone into").option("--dry-run", "Print the plan; mutate nothing").action(async (name, opts) => {
|
|
1695
|
+
process.exit(await runNew(name, { run: opts.run, dir: opts.dir, dryRun: opts.dryRun }));
|
|
1696
|
+
});
|
|
1697
|
+
pebbleCmd.command("provision <provider> <slug>").description("Run one provider step against an existing pebble").option("--dry-run", "Print; mutate nothing").action(async (provider, slug, opts) => {
|
|
1698
|
+
const step = STEP_REGISTRY[provider];
|
|
1699
|
+
if (!step) {
|
|
1700
|
+
console.error(`Unknown provider: ${provider}`);
|
|
1701
|
+
process.exit(1);
|
|
1702
|
+
}
|
|
1703
|
+
const { report, halted } = await runProvisioner(
|
|
1704
|
+
{ slug, name: slug, repoDir: null, dryRun: !!opts.dryRun, captured: {} },
|
|
1705
|
+
[step]
|
|
1706
|
+
);
|
|
1707
|
+
process.stdout.write(JSON.stringify(report) + "\n");
|
|
1708
|
+
process.exit(halted && report.steps[0]?.status === "failed" ? 1 : 0);
|
|
1709
|
+
});
|
|
1710
|
+
|
|
1277
1711
|
// src/index.ts
|
|
1278
1712
|
import { readFileSync as readFileSync14 } from "fs";
|
|
1279
1713
|
import { fileURLToPath } from "url";
|
|
1280
|
-
import { dirname as dirname2, resolve } from "path";
|
|
1714
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
1281
1715
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
1282
|
-
var pkg = JSON.parse(readFileSync14(
|
|
1716
|
+
var pkg = JSON.parse(readFileSync14(resolve2(__dirname, "../package.json"), "utf-8"));
|
|
1283
1717
|
var GOLD = "\x1B[33m";
|
|
1284
1718
|
var DIM = "\x1B[2m";
|
|
1285
1719
|
var RESET = "\x1B[0m";
|
|
@@ -1297,7 +1731,7 @@ ${RESET}
|
|
|
1297
1731
|
${DIM}${cwd}${RESET}
|
|
1298
1732
|
`);
|
|
1299
1733
|
}
|
|
1300
|
-
var program = new
|
|
1734
|
+
var program = new Command21();
|
|
1301
1735
|
program.name("odin").description("CLI for Odin \u2014 the knowledge backbone for Pebble House").version(VERSION);
|
|
1302
1736
|
program.command("login").description("Authenticate with Odin via browser").action(async () => {
|
|
1303
1737
|
printBanner();
|
|
@@ -1331,6 +1765,7 @@ program.addCommand(roadmapsCmd);
|
|
|
1331
1765
|
program.addCommand(personaRunsCmd);
|
|
1332
1766
|
program.addCommand(blogCmd);
|
|
1333
1767
|
program.addCommand(apiKeysCmd);
|
|
1768
|
+
program.addCommand(pebbleCmd);
|
|
1334
1769
|
process.on("exit", () => {
|
|
1335
1770
|
process.stderr.write("\n\n\n\n\n");
|
|
1336
1771
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pebblehouse/odin-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI for Odin — the knowledge backbone for Pebble House",
|
|
6
6
|
"bin": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"node": ">=18"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@supabase/supabase-js": "^2.
|
|
37
|
+
"@supabase/supabase-js": "^2.105.0",
|
|
38
38
|
"commander": "^12.0.0",
|
|
39
39
|
"open": "^10.0.0"
|
|
40
40
|
},
|