@biffo/cli 0.41.0 → 0.41.2

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/core.version CHANGED
@@ -1 +1 @@
1
- 0.41.0
1
+ 0.41.2
package/dist/index.js CHANGED
@@ -599,9 +599,19 @@ var GitHubAdapter = class {
599
599
  async createEmptyRepo(org, repo, description) {
600
600
  try {
601
601
  const { data: existing } = await this.octokit.repos.get({ owner: org, repo });
602
- log.info(`Repository ${org}/${repo} already exists \u2014 skipping creation`);
602
+ if (await this.repoHasCommits(org, repo)) {
603
+ throw new Error(
604
+ `Repository ${org}/${repo} already exists and is not empty.
605
+ Biffo will not push the sibling skeleton over a repo that has content.
606
+ If this repo is a half-finished Biffo sibling you want to redo, delete it
607
+ (gh repo delete ${org}/${repo}) and re-run. If it is unrelated, choose a
608
+ different sibling name.`
609
+ );
610
+ }
611
+ log.info(`Repository ${org}/${repo} already exists and is empty \u2014 adopting it`);
603
612
  return existing.clone_url;
604
613
  } catch (err) {
614
+ if (err instanceof Error && !("status" in err)) throw err;
605
615
  if (err.status !== 404) throw err;
606
616
  }
607
617
  log.info(`Creating repository ${org}/${repo}...`);
@@ -625,6 +635,27 @@ var GitHubAdapter = class {
625
635
  log.success(`Repository created: ${data.html_url}`);
626
636
  return data.clone_url;
627
637
  }
638
+ /**
639
+ * Does `org/repo` have any commits on any branch?
640
+ *
641
+ * `repos.get`'s `size` is unreliable for this — it is in KB and reports 0 for
642
+ * a freshly-pushed small repo. GitHub answers the question directly instead:
643
+ * listing commits on a repo with no commits returns 409 ("Git Repository is
644
+ * empty"). A 404 means the repo itself is gone, which is also "no commits".
645
+ *
646
+ * Used to decide whether an existing repo is safe to adopt (issue #316) and
647
+ * whether a skeleton push has already happened.
648
+ */
649
+ async repoHasCommits(org, repo) {
650
+ try {
651
+ const { data } = await this.octokit.repos.listCommits({ owner: org, repo, per_page: 1 });
652
+ return data.length > 0;
653
+ } catch (err) {
654
+ const status = err.status;
655
+ if (status === 409 || status === 404) return false;
656
+ throw err;
657
+ }
658
+ }
628
659
  async deleteRepo(org, repo) {
629
660
  try {
630
661
  await this.octokit.repos.get({ owner: org, repo });
@@ -2197,6 +2228,13 @@ import {
2197
2228
  } from "fs";
2198
2229
  import { homedir } from "os";
2199
2230
  import { join as join9 } from "path";
2231
+ var LEGACY_STEP_ALIASES = {
2232
+ github_config: ["github_branches", "github_instance_files", "github_settings"]
2233
+ };
2234
+ function hasCompleted(session, step) {
2235
+ if (session.completedSteps.includes(step)) return true;
2236
+ return session.completedSteps.some((done) => LEGACY_STEP_ALIASES[done]?.includes(step) ?? false);
2237
+ }
2200
2238
  function sessionsDir() {
2201
2239
  return process.env["BIFFO_SESSIONS_DIR"] ?? join9(homedir(), ".biffo", "sessions");
2202
2240
  }
@@ -2232,8 +2270,18 @@ function saveSession(session) {
2232
2270
  const dir = sessionsDir();
2233
2271
  if (!existsSync6(dir)) mkdirSync3(dir, { recursive: true });
2234
2272
  const name = session.config.project?.name ?? "unknown";
2273
+ const prior = loadSession(name);
2274
+ if (prior) {
2275
+ for (const step of prior.completedSteps) {
2276
+ if (!session.completedSteps.includes(step)) session.completedSteps.push(step);
2277
+ }
2278
+ session.outputs = { ...prior.outputs, ...definedOnly(session.outputs) };
2279
+ }
2235
2280
  writeFileSync4(sessionPath(name), JSON.stringify(session, null, 2));
2236
2281
  }
2282
+ function definedOnly(value) {
2283
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
2284
+ }
2237
2285
  function markStepComplete(session, step) {
2238
2286
  if (!session.completedSteps.includes(step)) {
2239
2287
  session.completedSteps.push(step);
@@ -3554,64 +3602,29 @@ var SiblingConfigSchema = z4.object({
3554
3602
  }
3555
3603
  });
3556
3604
 
3557
- // src/commands/sibling-create.ts
3558
- import { cpSync as cpSync2, existsSync as existsSync14, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync4, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
3559
- import { tmpdir as tmpdir4 } from "os";
3560
- import { dirname as dirname6, join as join16, resolve as resolve9 } from "path";
3561
- import { fileURLToPath as fileURLToPath4 } from "url";
3562
- import chalk11 from "chalk";
3563
- import { Command as Command11 } from "commander";
3564
-
3565
- // src/lib/skeleton-dotfiles.ts
3566
- import { readdirSync as readdirSync7, renameSync } from "fs";
3567
- import { join as join14 } from "path";
3568
- var PACKAGED_GITIGNORE = "_gitignore";
3569
- var REAL_GITIGNORE = ".gitignore";
3570
- function restorePackagedDotfiles(dir) {
3571
- const restored = [];
3572
- for (const entry of readdirSync7(dir, { withFileTypes: true })) {
3573
- const full = join14(dir, entry.name);
3574
- if (entry.isDirectory()) {
3575
- restored.push(...restorePackagedDotfiles(full));
3576
- } else if (entry.name === PACKAGED_GITIGNORE) {
3577
- const target = join14(dir, REAL_GITIGNORE);
3578
- renameSync(full, target);
3579
- restored.push(target);
3580
- }
3581
- }
3582
- return restored;
3583
- }
3584
-
3585
3605
  // src/lib/sibling-session.ts
3586
3606
  import {
3587
3607
  existsSync as existsSync13,
3588
3608
  mkdirSync as mkdirSync5,
3589
- readdirSync as readdirSync8,
3609
+ readdirSync as readdirSync7,
3590
3610
  readFileSync as readFileSync10,
3591
3611
  rmSync as rmSync5,
3592
3612
  statSync as statSync4,
3593
3613
  writeFileSync as writeFileSync5
3594
3614
  } from "fs";
3595
3615
  import { homedir as homedir3 } from "os";
3596
- import { join as join15 } from "path";
3616
+ import { join as join14 } from "path";
3597
3617
  function sessionsDir2() {
3598
- return process.env["BIFFO_SIBLING_SESSIONS_DIR"] ?? join15(homedir3(), ".biffo", "sibling-sessions");
3618
+ return process.env["BIFFO_SIBLING_SESSIONS_DIR"] ?? join14(homedir3(), ".biffo", "sibling-sessions");
3599
3619
  }
3600
3620
  function sessionPath2(projectName) {
3601
- return join15(sessionsDir2(), `${projectName}.json`);
3621
+ return join14(sessionsDir2(), `${projectName}.json`);
3602
3622
  }
3603
- function findLatestSiblingSession() {
3604
- const dir = sessionsDir2();
3605
- if (!existsSync13(dir)) return null;
3606
- const files = readdirSync8(dir).filter((f) => f.endsWith(".json"));
3607
- if (files.length === 0) return null;
3608
- const sorted = files.map((f) => {
3609
- const fullPath = join15(dir, f);
3610
- const mtime = existsSync13(fullPath) ? statSync4(fullPath).mtimeMs : -1;
3611
- return { f, mtime };
3612
- }).sort((a, b) => b.mtime - a.mtime);
3623
+ function loadSiblingSession(projectName) {
3624
+ const path = sessionPath2(projectName);
3625
+ if (!existsSync13(path)) return null;
3613
3626
  try {
3614
- return JSON.parse(readFileSync10(join15(dir, sorted[0].f), "utf8"));
3627
+ return JSON.parse(readFileSync10(path, "utf8"));
3615
3628
  } catch {
3616
3629
  return null;
3617
3630
  }
@@ -3620,8 +3633,18 @@ function saveSiblingSession(session) {
3620
3633
  const dir = sessionsDir2();
3621
3634
  if (!existsSync13(dir)) mkdirSync5(dir, { recursive: true });
3622
3635
  const name = session.config.project?.name ?? "unknown";
3636
+ const prior = loadSiblingSession(name);
3637
+ if (prior) {
3638
+ for (const step of prior.completedSteps) {
3639
+ if (!session.completedSteps.includes(step)) session.completedSteps.push(step);
3640
+ }
3641
+ session.outputs = { ...prior.outputs, ...definedOnly2(session.outputs) };
3642
+ }
3623
3643
  writeFileSync5(sessionPath2(name), JSON.stringify(session, null, 2));
3624
3644
  }
3645
+ function definedOnly2(value) {
3646
+ return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== void 0));
3647
+ }
3625
3648
  function markSiblingStepComplete(session, step) {
3626
3649
  if (!session.completedSteps.includes(step)) {
3627
3650
  session.completedSteps.push(step);
@@ -3633,6 +3656,34 @@ function deleteSiblingSession(projectName) {
3633
3656
  if (existsSync13(path)) rmSync5(path);
3634
3657
  }
3635
3658
 
3659
+ // src/commands/sibling-create.ts
3660
+ import { cpSync as cpSync2, existsSync as existsSync14, mkdirSync as mkdirSync6, mkdtempSync as mkdtempSync4, readFileSync as readFileSync11, writeFileSync as writeFileSync6 } from "fs";
3661
+ import { tmpdir as tmpdir4 } from "os";
3662
+ import { dirname as dirname6, join as join16, resolve as resolve9 } from "path";
3663
+ import { fileURLToPath as fileURLToPath4 } from "url";
3664
+ import chalk11 from "chalk";
3665
+ import { Command as Command11 } from "commander";
3666
+
3667
+ // src/lib/skeleton-dotfiles.ts
3668
+ import { readdirSync as readdirSync8, renameSync } from "fs";
3669
+ import { join as join15 } from "path";
3670
+ var PACKAGED_GITIGNORE = "_gitignore";
3671
+ var REAL_GITIGNORE = ".gitignore";
3672
+ function restorePackagedDotfiles(dir) {
3673
+ const restored = [];
3674
+ for (const entry of readdirSync8(dir, { withFileTypes: true })) {
3675
+ const full = join15(dir, entry.name);
3676
+ if (entry.isDirectory()) {
3677
+ restored.push(...restorePackagedDotfiles(full));
3678
+ } else if (entry.name === PACKAGED_GITIGNORE) {
3679
+ const target = join15(dir, REAL_GITIGNORE);
3680
+ renameSync(full, target);
3681
+ restored.push(target);
3682
+ }
3683
+ }
3684
+ return restored;
3685
+ }
3686
+
3636
3687
  // src/commands/sibling-create.ts
3637
3688
  var siblingCreateCommand = new Command11("create").description(
3638
3689
  "Create a standalone sibling app repository from the Biffo sibling template (ADR-0007)"
@@ -3675,8 +3726,8 @@ async function runSiblingCreateCommand(name, options) {
3675
3726
  }
3676
3727
  let session = null;
3677
3728
  if (!options.fresh) {
3678
- const saved = findLatestSiblingSession();
3679
- if (saved && saved.config.project?.name === name) {
3729
+ const saved = loadSiblingSession(name);
3730
+ if (saved) {
3680
3731
  session = saved;
3681
3732
  console.log(
3682
3733
  chalk11.dim(
@@ -3687,6 +3738,7 @@ async function runSiblingCreateCommand(name, options) {
3687
3738
  }
3688
3739
  }
3689
3740
  if (!session) {
3741
+ deleteSiblingSession(name);
3690
3742
  session = {
3691
3743
  version: 1,
3692
3744
  config,
@@ -3732,7 +3784,7 @@ async function runSiblingCreateCommand(name, options) {
3732
3784
  );
3733
3785
  }
3734
3786
  async function runSiblingCreate(github, aws, coreAws, git, config, session, options) {
3735
- const totalSteps = 7;
3787
+ const totalSteps = 8;
3736
3788
  const { org, repo } = githubRepo(config);
3737
3789
  const pathPrefix = resolvePathPrefix(config);
3738
3790
  if (!session.completedSteps.includes("verify_credentials")) {
@@ -3768,54 +3820,67 @@ async function runSiblingCreate(github, aws, coreAws, git, config, session, opti
3768
3820
  );
3769
3821
  }
3770
3822
  if (!session.completedSteps.includes("create_repo")) {
3771
- log.step(3, totalSteps, "Creating GitHub repository and pushing sibling skeleton...");
3772
- const cloneUrl = await github.createEmptyRepo(
3823
+ log.step(3, totalSteps, "Creating GitHub repository...");
3824
+ session.outputs.cloneUrl = await github.createEmptyRepo(
3773
3825
  org,
3774
3826
  repo,
3775
3827
  config.project.description || void 0
3776
3828
  );
3777
- session.outputs.cloneUrl = cloneUrl;
3778
- await pushSkeleton(
3779
- git,
3780
- options.skeletonRoot,
3781
- cloneUrl,
3782
- config,
3783
- options.coreConfig,
3784
- options.githubToken
3785
- );
3786
3829
  markSiblingStepComplete(session, "create_repo");
3787
3830
  } else {
3788
3831
  log.step(3, totalSteps, "GitHub repository already created \u2014 skipping");
3789
3832
  }
3833
+ const cloneUrl = session.outputs.cloneUrl;
3834
+ if (!cloneUrl) {
3835
+ throw new Error("internal error: create_repo did not populate session.outputs.cloneUrl");
3836
+ }
3837
+ if (!session.completedSteps.includes("push_skeleton")) {
3838
+ if (await github.repoHasCommits(org, repo)) {
3839
+ log.step(4, totalSteps, "Sibling skeleton already pushed \u2014 skipping");
3840
+ } else {
3841
+ log.step(4, totalSteps, "Pushing sibling skeleton...");
3842
+ await pushSkeleton(
3843
+ git,
3844
+ options.skeletonRoot,
3845
+ cloneUrl,
3846
+ config,
3847
+ options.coreConfig,
3848
+ options.githubToken
3849
+ );
3850
+ }
3851
+ markSiblingStepComplete(session, "push_skeleton");
3852
+ } else {
3853
+ log.step(4, totalSteps, "Sibling skeleton already pushed \u2014 skipping");
3854
+ }
3790
3855
  if (!session.completedSteps.includes("oidc_trust")) {
3791
- log.step(4, totalSteps, "Configuring OIDC trust...");
3856
+ log.step(5, totalSteps, "Configuring OIDC trust...");
3792
3857
  session.outputs.oidcRoleArn = await aws.setupOidcTrust(
3793
3858
  config,
3794
3859
  await resolveRepoIds(github, config)
3795
3860
  );
3796
3861
  markSiblingStepComplete(session, "oidc_trust");
3797
3862
  } else {
3798
- log.step(4, totalSteps, "OIDC trust already configured \u2014 skipping");
3863
+ log.step(5, totalSteps, "OIDC trust already configured \u2014 skipping");
3799
3864
  }
3800
3865
  if (!session.completedSteps.includes("terraform_backend")) {
3801
- log.step(5, totalSteps, "Bootstrapping Terraform state backend...");
3866
+ log.step(6, totalSteps, "Bootstrapping Terraform state backend...");
3802
3867
  session.outputs.tfStateBucket = await aws.bootstrapTerraformBackend(config.project.name);
3803
3868
  markSiblingStepComplete(session, "terraform_backend");
3804
3869
  } else {
3805
- log.step(5, totalSteps, "Terraform backend already bootstrapped \u2014 skipping");
3870
+ log.step(6, totalSteps, "Terraform backend already bootstrapped \u2014 skipping");
3806
3871
  }
3807
3872
  if (!session.completedSteps.includes("github_config")) {
3808
- log.step(6, totalSteps, "Configuring GitHub repository...");
3873
+ log.step(7, totalSteps, "Configuring GitHub repository...");
3809
3874
  await configureSiblingGithub(github, config, options.coreConfig, session, coreIdentity);
3810
3875
  markSiblingStepComplete(session, "github_config");
3811
3876
  } else {
3812
- log.step(6, totalSteps, "GitHub already configured \u2014 skipping");
3877
+ log.step(7, totalSteps, "GitHub already configured \u2014 skipping");
3813
3878
  }
3814
3879
  if (options.skipRegistration && !session.completedSteps.includes("register_with_core")) {
3815
- log.step(7, totalSteps, "Already registered with the core project by `biffo init` \u2014 skipping");
3880
+ log.step(8, totalSteps, "Already registered with the core project by `biffo init` \u2014 skipping");
3816
3881
  markSiblingStepComplete(session, "register_with_core");
3817
3882
  } else if (!session.completedSteps.includes("register_with_core")) {
3818
- log.step(7, totalSteps, "Opening a registration PR against the core project...");
3883
+ log.step(8, totalSteps, "Opening a registration PR against the core project...");
3819
3884
  session.outputs.registrationPrUrl = await registerWithCore(
3820
3885
  git,
3821
3886
  github,
@@ -3826,7 +3891,7 @@ async function runSiblingCreate(github, aws, coreAws, git, config, session, opti
3826
3891
  );
3827
3892
  markSiblingStepComplete(session, "register_with_core");
3828
3893
  } else {
3829
- log.step(7, totalSteps, "Already registered with the core project \u2014 skipping");
3894
+ log.step(8, totalSteps, "Already registered with the core project \u2014 skipping");
3830
3895
  }
3831
3896
  deleteSiblingSession(config.project.name);
3832
3897
  }
@@ -3972,7 +4037,10 @@ async function configureSiblingGithub(github, config, coreConfig, session, coreI
3972
4037
  await github.createEnvironments(config);
3973
4038
  await github.enableVulnerabilityAlerts(org, repo);
3974
4039
  await github.setRepoVariable(org, repo, "PROJECT_NAME", config.project.name);
3975
- await github.setRepoVariable(org, repo, "PATH_PREFIX", resolvePathPrefix(config));
4040
+ const pathPrefix = resolvePathPrefix(config);
4041
+ if (pathPrefix !== "") {
4042
+ await github.setRepoVariable(org, repo, "PATH_PREFIX", pathPrefix);
4043
+ }
3976
4044
  await github.setRepoVariable(org, repo, "AWS_REGION", awsConfig(config).region);
3977
4045
  await github.setRepoVariable(org, repo, "SIBLING_DEPLOY_ENABLED", "true");
3978
4046
  try {
@@ -4178,14 +4246,7 @@ var initCommand = new Command12("init").description("Scaffold a new project from
4178
4246
  const rawConfig = JSON.parse(readFileSync12(resolve10(options.config), "utf8"));
4179
4247
  config = parseConfig(rawConfig);
4180
4248
  const { account_id: accountId, region } = config.cloud.config;
4181
- session = {
4182
- version: 1,
4183
- config,
4184
- awsAccountId: accountId,
4185
- awsRegion: region,
4186
- completedSteps: [],
4187
- outputs: {}
4188
- };
4249
+ session = resolveConfigFileSession(config, accountId, region, options.fresh === true);
4189
4250
  } else {
4190
4251
  githubToken = await resolveGithubToken3(options.yes === true);
4191
4252
  const { accountId, region, profile } = await resolveAwsCredentials(options.yes === true);
@@ -4224,6 +4285,7 @@ var initCommand = new Command12("init").description("Scaffold a new project from
4224
4285
  if (!session) {
4225
4286
  const rawConfig = await promptForConfig(accountId, region, profile);
4226
4287
  config = parseConfig(rawConfig);
4288
+ deleteSession(config.project.name);
4227
4289
  session = {
4228
4290
  version: 1,
4229
4291
  config,
@@ -4267,6 +4329,22 @@ var initCommand = new Command12("init").description("Scaffold a new project from
4267
4329
  );
4268
4330
  }
4269
4331
  );
4332
+ function resolveConfigFileSession(config, awsAccountId, awsRegion, fresh) {
4333
+ const saved = fresh ? null : loadSession(config.project.name);
4334
+ const session = saved ? { ...saved, config, awsAccountId, awsRegion } : { version: 1, config, awsAccountId, awsRegion, completedSteps: [], outputs: {} };
4335
+ if (saved) {
4336
+ console.log(
4337
+ chalk12.dim(
4338
+ ` Resuming previous init for ${config.project.name} (completed: ${saved.completedSteps.join(", ") || "none"})
4339
+ `
4340
+ )
4341
+ );
4342
+ } else {
4343
+ deleteSession(config.project.name);
4344
+ }
4345
+ saveSession(session);
4346
+ return session;
4347
+ }
4270
4348
  async function runInit(github, aws, config, session, appSibling) {
4271
4349
  const totalSteps = appSibling ? 6 : 5;
4272
4350
  if (!session.completedSteps.includes("verify_credentials")) {
@@ -4320,14 +4398,26 @@ async function runInit(github, aws, config, session, appSibling) {
4320
4398
  };
4321
4399
  }
4322
4400
  }
4323
- if (!session.completedSteps.includes("github_config")) {
4324
- log.step(5, totalSteps, "Configuring GitHub repository...");
4325
- const { org, repo } = config.source_control.config;
4326
- const dns = resolveDnsConfig(config);
4327
- const domain = dns.domain;
4401
+ const { org, repo } = config.source_control.config;
4402
+ if (!hasCompleted(session, "github_branches")) {
4403
+ log.step(5, totalSteps, "Creating dev and staging branches...");
4328
4404
  await github.createBranch(org, repo, "dev", "main");
4329
4405
  await github.createBranch(org, repo, "staging", "main");
4406
+ markStepComplete(session, "github_branches");
4407
+ } else {
4408
+ log.step(5, totalSteps, "Branches already created \u2014 skipping");
4409
+ }
4410
+ if (!hasCompleted(session, "github_instance_files")) {
4411
+ log.step(5, totalSteps, "Writing instance identity files...");
4330
4412
  await writeInstanceFiles(github, org, repo, config);
4413
+ markStepComplete(session, "github_instance_files");
4414
+ } else {
4415
+ log.step(5, totalSteps, "Instance identity files already written \u2014 skipping");
4416
+ }
4417
+ if (!hasCompleted(session, "github_settings")) {
4418
+ log.step(5, totalSteps, "Configuring GitHub repository...");
4419
+ const dns = resolveDnsConfig(config);
4420
+ const domain = dns.domain;
4331
4421
  await github.setDefaultBranch(org, repo, "dev");
4332
4422
  await github.configureBranchProtection(config);
4333
4423
  await github.createEnvironments(config);
@@ -4352,7 +4442,7 @@ async function runInit(github, aws, config, session, appSibling) {
4352
4442
  if (session.outputs.oidcRoleArn) {
4353
4443
  await github.setRepoSecret(org, repo, "BIFFO_OIDC_ROLE_ARN", session.outputs.oidcRoleArn);
4354
4444
  }
4355
- markStepComplete(session, "github_config");
4445
+ markStepComplete(session, "github_settings");
4356
4446
  } else {
4357
4447
  log.step(5, totalSteps, "GitHub already configured \u2014 skipping");
4358
4448
  }
@@ -4371,6 +4461,9 @@ async function runInit(github, aws, config, session, appSibling) {
4371
4461
  async function createAppSibling(github, config, session, deps) {
4372
4462
  const siblingConfig = appSiblingConfig(config);
4373
4463
  const cloud = config.cloud;
4464
+ if (!session.outputs.appSibling) {
4465
+ deleteSiblingSession(siblingConfig.project.name);
4466
+ }
4374
4467
  session.outputs.appSibling ??= {
4375
4468
  version: 1,
4376
4469
  config: siblingConfig,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.41.0",
3
+ "version": "0.41.2",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",