codebyplan 1.13.16 → 1.13.17
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/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ var VERSION, PACKAGE_NAME;
|
|
|
14
14
|
var init_version = __esm({
|
|
15
15
|
"src/lib/version.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
VERSION = "1.13.
|
|
17
|
+
VERSION = "1.13.17";
|
|
18
18
|
PACKAGE_NAME = "codebyplan";
|
|
19
19
|
}
|
|
20
20
|
});
|
|
@@ -663,10 +663,8 @@ function noAuthHint() {
|
|
|
663
663
|
Or, for the legacy 30-day shim:
|
|
664
664
|
export CODEBYPLAN_API_KEY=<key> # from https://codebyplan.com/settings/api-keys/`;
|
|
665
665
|
}
|
|
666
|
-
function
|
|
667
|
-
|
|
668
|
-
throw new Error(noAuthHint());
|
|
669
|
-
}
|
|
666
|
+
async function validateAuth() {
|
|
667
|
+
await getAuthHeaders();
|
|
670
668
|
}
|
|
671
669
|
async function getAuthHeaders() {
|
|
672
670
|
try {
|
|
@@ -3797,7 +3795,7 @@ async function eslintInit(repoId, projectPath) {
|
|
|
3797
3795
|
async function runEslint() {
|
|
3798
3796
|
const subcommand = process.argv[3];
|
|
3799
3797
|
const flags = parseFlags(4);
|
|
3800
|
-
|
|
3798
|
+
await validateAuth();
|
|
3801
3799
|
const config = await resolveConfig(flags);
|
|
3802
3800
|
const { repoId, projectPath } = config;
|
|
3803
3801
|
switch (subcommand) {
|
|
@@ -7073,6 +7071,94 @@ var init_cmux_serve = __esm({
|
|
|
7073
7071
|
}
|
|
7074
7072
|
});
|
|
7075
7073
|
|
|
7074
|
+
// src/lib/worktree-port-resolver.ts
|
|
7075
|
+
async function resolveWorktreePortAllocations(repoId, projectPath) {
|
|
7076
|
+
let resolvedWorktreeId;
|
|
7077
|
+
try {
|
|
7078
|
+
const deviceId = await getOrCreateDeviceId(projectPath);
|
|
7079
|
+
let branch = "main";
|
|
7080
|
+
try {
|
|
7081
|
+
const { execSync: execSync11 } = await import("node:child_process");
|
|
7082
|
+
branch = execSync11("git symbolic-ref --short HEAD", {
|
|
7083
|
+
cwd: projectPath,
|
|
7084
|
+
encoding: "utf-8"
|
|
7085
|
+
}).trim();
|
|
7086
|
+
} catch {
|
|
7087
|
+
}
|
|
7088
|
+
const tupleId = await resolveWorktreeId({
|
|
7089
|
+
repoId,
|
|
7090
|
+
repoPath: projectPath,
|
|
7091
|
+
branch,
|
|
7092
|
+
deviceId
|
|
7093
|
+
});
|
|
7094
|
+
if (tupleId) {
|
|
7095
|
+
resolvedWorktreeId = tupleId;
|
|
7096
|
+
} else {
|
|
7097
|
+
resolvedWorktreeId = await resolveAndCacheWorktreeId(repoId, projectPath) ?? void 0;
|
|
7098
|
+
}
|
|
7099
|
+
} catch (err) {
|
|
7100
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
7101
|
+
console.warn(
|
|
7102
|
+
` Warning: failed to cache worktree_id (self-heal skipped): ${msg}`
|
|
7103
|
+
);
|
|
7104
|
+
}
|
|
7105
|
+
let portAllocations = [];
|
|
7106
|
+
try {
|
|
7107
|
+
const portsRes = await apiGet(
|
|
7108
|
+
`/port-allocations`,
|
|
7109
|
+
resolvedWorktreeId ? {
|
|
7110
|
+
repo_id: repoId,
|
|
7111
|
+
worktree_id: resolvedWorktreeId,
|
|
7112
|
+
limit: PORT_ALLOCATIONS_UNFILTERED_LIMIT
|
|
7113
|
+
} : {
|
|
7114
|
+
repo_id: repoId,
|
|
7115
|
+
worktree_id: "null",
|
|
7116
|
+
limit: PORT_ALLOCATIONS_UNFILTERED_LIMIT
|
|
7117
|
+
}
|
|
7118
|
+
);
|
|
7119
|
+
const allAllocations = portsRes.data ?? [];
|
|
7120
|
+
const filtered = resolvedWorktreeId ? allAllocations.filter((a) => a.worktree_id === resolvedWorktreeId) : allAllocations.filter((a) => !a.worktree_id);
|
|
7121
|
+
portAllocations = filtered.map((a) => {
|
|
7122
|
+
const clean = {};
|
|
7123
|
+
for (const key of ALLOWED_FIELDS) {
|
|
7124
|
+
if (key in a) clean[key] = a[key];
|
|
7125
|
+
}
|
|
7126
|
+
return clean;
|
|
7127
|
+
});
|
|
7128
|
+
} catch (err) {
|
|
7129
|
+
console.warn(
|
|
7130
|
+
` Warning: failed to fetch port allocations: ${err instanceof Error ? err.message : String(err)}`
|
|
7131
|
+
);
|
|
7132
|
+
}
|
|
7133
|
+
const matchingAlloc = portAllocations[0];
|
|
7134
|
+
return { resolvedWorktreeId, portAllocations, matchingAlloc };
|
|
7135
|
+
}
|
|
7136
|
+
var PORT_ALLOCATIONS_UNFILTERED_LIMIT, ALLOWED_FIELDS;
|
|
7137
|
+
var init_worktree_port_resolver = __esm({
|
|
7138
|
+
"src/lib/worktree-port-resolver.ts"() {
|
|
7139
|
+
"use strict";
|
|
7140
|
+
init_api();
|
|
7141
|
+
init_resolve_worktree();
|
|
7142
|
+
init_local_config();
|
|
7143
|
+
PORT_ALLOCATIONS_UNFILTERED_LIMIT = "500";
|
|
7144
|
+
ALLOWED_FIELDS = [
|
|
7145
|
+
"id",
|
|
7146
|
+
"repo_id",
|
|
7147
|
+
"port",
|
|
7148
|
+
"label",
|
|
7149
|
+
"server_type",
|
|
7150
|
+
"auto_start",
|
|
7151
|
+
"command",
|
|
7152
|
+
"working_dir",
|
|
7153
|
+
"env_vars",
|
|
7154
|
+
"external_refs",
|
|
7155
|
+
"worktree_id",
|
|
7156
|
+
"created_at",
|
|
7157
|
+
"updated_at"
|
|
7158
|
+
];
|
|
7159
|
+
}
|
|
7160
|
+
});
|
|
7161
|
+
|
|
7076
7162
|
// src/lib/migrate-local-config.ts
|
|
7077
7163
|
import { mkdir as mkdir6, readFile as readFile16, unlink as unlink2, writeFile as writeFile12 } from "node:fs/promises";
|
|
7078
7164
|
import { join as join24 } from "node:path";
|
|
@@ -7307,6 +7393,7 @@ __export(config_exports, {
|
|
|
7307
7393
|
readGitConfig: () => readGitConfig,
|
|
7308
7394
|
readRepoConfig: () => readRepoConfig,
|
|
7309
7395
|
readServerConfig: () => readServerConfig,
|
|
7396
|
+
readServerLocalConfig: () => readServerLocalConfig,
|
|
7310
7397
|
readShipmentConfig: () => readShipmentConfig,
|
|
7311
7398
|
readVendorConfig: () => readVendorConfig,
|
|
7312
7399
|
runConfig: () => runConfig
|
|
@@ -7316,7 +7403,7 @@ import { join as join25 } from "node:path";
|
|
|
7316
7403
|
async function runConfig() {
|
|
7317
7404
|
const flags = parseFlags(3);
|
|
7318
7405
|
const dryRun = hasFlag("dry-run", 3);
|
|
7319
|
-
|
|
7406
|
+
await validateAuth();
|
|
7320
7407
|
const config = await resolveConfig(flags);
|
|
7321
7408
|
const { repoId, projectPath } = config;
|
|
7322
7409
|
console.log(`
|
|
@@ -7346,35 +7433,12 @@ async function runConfig() {
|
|
|
7346
7433
|
}
|
|
7347
7434
|
async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
7348
7435
|
const codebyplanDir = join25(projectPath, ".codebyplan");
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
branch = execSync11("git symbolic-ref --short HEAD", {
|
|
7356
|
-
cwd: projectPath,
|
|
7357
|
-
encoding: "utf-8"
|
|
7358
|
-
}).trim();
|
|
7359
|
-
} catch {
|
|
7360
|
-
}
|
|
7361
|
-
const tupleId = await resolveWorktreeId({
|
|
7362
|
-
repoId,
|
|
7363
|
-
repoPath: projectPath,
|
|
7364
|
-
branch,
|
|
7365
|
-
deviceId
|
|
7366
|
-
});
|
|
7367
|
-
if (tupleId) {
|
|
7368
|
-
resolvedWorktreeId = tupleId;
|
|
7369
|
-
} else {
|
|
7370
|
-
resolvedWorktreeId = await resolveAndCacheWorktreeId(repoId, projectPath) ?? void 0;
|
|
7371
|
-
}
|
|
7372
|
-
} catch (err) {
|
|
7373
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
7374
|
-
console.warn(
|
|
7375
|
-
` Warning: failed to cache worktree_id (self-heal skipped): ${msg}`
|
|
7376
|
-
);
|
|
7377
|
-
}
|
|
7436
|
+
const {
|
|
7437
|
+
resolvedWorktreeId,
|
|
7438
|
+
portAllocations,
|
|
7439
|
+
matchingAlloc: matchingAllocRaw
|
|
7440
|
+
} = await resolveWorktreePortAllocations(repoId, projectPath);
|
|
7441
|
+
const matchingAlloc = matchingAllocRaw;
|
|
7378
7442
|
let repoRes;
|
|
7379
7443
|
try {
|
|
7380
7444
|
repoRes = await apiGet(`/repos/${repoId}`);
|
|
@@ -7397,42 +7461,6 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
|
7397
7461
|
process.exit(1);
|
|
7398
7462
|
}
|
|
7399
7463
|
const repo = repoRes.data;
|
|
7400
|
-
let portAllocations = [];
|
|
7401
|
-
try {
|
|
7402
|
-
const portsRes = await apiGet(
|
|
7403
|
-
`/port-allocations`,
|
|
7404
|
-
{ repo_id: repoId }
|
|
7405
|
-
);
|
|
7406
|
-
const allAllocations = portsRes.data ?? [];
|
|
7407
|
-
const filtered = resolvedWorktreeId ? allAllocations.filter((a) => a.worktree_id === resolvedWorktreeId) : allAllocations.filter((a) => !a.worktree_id);
|
|
7408
|
-
const ALLOWED_FIELDS = [
|
|
7409
|
-
"id",
|
|
7410
|
-
"repo_id",
|
|
7411
|
-
"port",
|
|
7412
|
-
"label",
|
|
7413
|
-
"server_type",
|
|
7414
|
-
"auto_start",
|
|
7415
|
-
"command",
|
|
7416
|
-
"working_dir",
|
|
7417
|
-
"env_vars",
|
|
7418
|
-
"external_refs",
|
|
7419
|
-
"worktree_id",
|
|
7420
|
-
"created_at",
|
|
7421
|
-
"updated_at"
|
|
7422
|
-
];
|
|
7423
|
-
portAllocations = filtered.map((a) => {
|
|
7424
|
-
const clean = {};
|
|
7425
|
-
for (const key of ALLOWED_FIELDS) {
|
|
7426
|
-
if (key in a) clean[key] = a[key];
|
|
7427
|
-
}
|
|
7428
|
-
return clean;
|
|
7429
|
-
});
|
|
7430
|
-
} catch (err) {
|
|
7431
|
-
console.warn(
|
|
7432
|
-
` Warning: failed to fetch port allocations: ${err instanceof Error ? err.message : String(err)}`
|
|
7433
|
-
);
|
|
7434
|
-
}
|
|
7435
|
-
const matchingAlloc = portAllocations[0];
|
|
7436
7464
|
const defaultBranchConfig = {
|
|
7437
7465
|
protected: ["main"],
|
|
7438
7466
|
integration: null,
|
|
@@ -7573,14 +7601,24 @@ async function readE2eConfig2(projectPath) {
|
|
|
7573
7601
|
return null;
|
|
7574
7602
|
}
|
|
7575
7603
|
}
|
|
7604
|
+
async function readServerLocalConfig(projectPath) {
|
|
7605
|
+
try {
|
|
7606
|
+
const raw = await readFile17(
|
|
7607
|
+
join25(projectPath, ".codebyplan", "server.local.json"),
|
|
7608
|
+
"utf-8"
|
|
7609
|
+
);
|
|
7610
|
+
return JSON.parse(raw);
|
|
7611
|
+
} catch {
|
|
7612
|
+
return null;
|
|
7613
|
+
}
|
|
7614
|
+
}
|
|
7576
7615
|
var legacyBranchConfigWarned;
|
|
7577
7616
|
var init_config = __esm({
|
|
7578
7617
|
"src/cli/config.ts"() {
|
|
7579
7618
|
"use strict";
|
|
7580
7619
|
init_flags();
|
|
7581
7620
|
init_api();
|
|
7582
|
-
|
|
7583
|
-
init_local_config();
|
|
7621
|
+
init_worktree_port_resolver();
|
|
7584
7622
|
init_migrate_local_config();
|
|
7585
7623
|
legacyBranchConfigWarned = false;
|
|
7586
7624
|
}
|
|
@@ -7738,22 +7776,48 @@ var init_port_verify = __esm({
|
|
|
7738
7776
|
// src/cli/ports.ts
|
|
7739
7777
|
var ports_exports = {};
|
|
7740
7778
|
__export(ports_exports, {
|
|
7779
|
+
parseEnvFile: () => parseEnvFile,
|
|
7741
7780
|
runPorts: () => runPorts
|
|
7742
7781
|
});
|
|
7782
|
+
import { mkdir as mkdir8, readFile as readFile19, writeFile as writeFile14 } from "node:fs/promises";
|
|
7783
|
+
import { join as join26 } from "node:path";
|
|
7743
7784
|
async function runPorts() {
|
|
7744
7785
|
const flags = parseFlags(3);
|
|
7745
7786
|
const dryRun = hasFlag("dry-run", 3);
|
|
7746
7787
|
const fix = hasFlag("fix", 3);
|
|
7747
|
-
|
|
7748
|
-
const
|
|
7749
|
-
|
|
7788
|
+
const writeLocal = hasFlag("write-local", 3);
|
|
7789
|
+
const provisionE2e = hasFlag("provision-e2e", 3);
|
|
7790
|
+
if (flags["path"]?.startsWith("--")) {
|
|
7791
|
+
console.warn(
|
|
7792
|
+
` Warning: --path value "${flags["path"]}" looks like a flag. Pass --path <dir> BEFORE boolean flags (e.g. --path . --provision-e2e).`
|
|
7793
|
+
);
|
|
7794
|
+
}
|
|
7795
|
+
const provisionE2eOnly = provisionE2e && !writeLocal;
|
|
7796
|
+
let repoId = "";
|
|
7797
|
+
let projectPath;
|
|
7798
|
+
if (provisionE2eOnly) {
|
|
7799
|
+
projectPath = flags["path"] ?? process.cwd();
|
|
7800
|
+
} else {
|
|
7801
|
+
await validateAuth();
|
|
7802
|
+
const config = await resolveConfig(flags);
|
|
7803
|
+
repoId = config.repoId;
|
|
7804
|
+
projectPath = config.projectPath;
|
|
7805
|
+
}
|
|
7750
7806
|
console.log(`
|
|
7751
7807
|
CodeByPlan Ports`);
|
|
7752
|
-
console.log(` Repo: ${repoId}`);
|
|
7808
|
+
if (repoId) console.log(` Repo: ${repoId}`);
|
|
7753
7809
|
console.log(` Path: ${projectPath}`);
|
|
7754
7810
|
if (dryRun) console.log(` Mode: dry-run`);
|
|
7755
7811
|
if (fix) console.log(` Mode: fix`);
|
|
7812
|
+
if (writeLocal) console.log(` Mode: write-local`);
|
|
7813
|
+
if (provisionE2e) console.log(` Mode: provision-e2e`);
|
|
7756
7814
|
console.log();
|
|
7815
|
+
if (writeLocal || provisionE2e) {
|
|
7816
|
+
if (writeLocal) await writeServerLocalConfig(repoId, projectPath, dryRun);
|
|
7817
|
+
if (provisionE2e) await provisionE2eEnv(projectPath, dryRun);
|
|
7818
|
+
console.log("\n Ports complete.\n");
|
|
7819
|
+
return;
|
|
7820
|
+
}
|
|
7757
7821
|
try {
|
|
7758
7822
|
const portsRes = await apiGet(
|
|
7759
7823
|
`/port-allocations`,
|
|
@@ -7822,12 +7886,135 @@ async function runPorts() {
|
|
|
7822
7886
|
}
|
|
7823
7887
|
console.log("\n Ports complete.\n");
|
|
7824
7888
|
}
|
|
7889
|
+
async function writeServerLocalConfig(repoId, projectPath, dryRun) {
|
|
7890
|
+
const { resolvedWorktreeId, portAllocations, matchingAlloc } = await resolveWorktreePortAllocations(repoId, projectPath);
|
|
7891
|
+
if (portAllocations.length === 0) {
|
|
7892
|
+
console.warn(
|
|
7893
|
+
" Skipped .codebyplan/server.local.json \u2014 no worktree port allocations resolved (API failure or none assigned)."
|
|
7894
|
+
);
|
|
7895
|
+
return;
|
|
7896
|
+
}
|
|
7897
|
+
const payload = {
|
|
7898
|
+
server_port: matchingAlloc?.port ?? null,
|
|
7899
|
+
server_type: matchingAlloc?.server_type ?? null,
|
|
7900
|
+
// No cast needed: resolveWorktreePortAllocations returns Partial<PortAllocation>[]
|
|
7901
|
+
// and ServerLocalConfig.port_allocations is typed the same — honest end-to-end.
|
|
7902
|
+
port_allocations: portAllocations
|
|
7903
|
+
};
|
|
7904
|
+
const codebyplanDir = join26(projectPath, ".codebyplan");
|
|
7905
|
+
const filePath = join26(codebyplanDir, "server.local.json");
|
|
7906
|
+
const newJson = JSON.stringify(payload, null, 2) + "\n";
|
|
7907
|
+
let currentJson = "";
|
|
7908
|
+
try {
|
|
7909
|
+
currentJson = await readFile19(filePath, "utf-8");
|
|
7910
|
+
} catch {
|
|
7911
|
+
}
|
|
7912
|
+
if (currentJson === newJson) {
|
|
7913
|
+
console.log(" server.local.json up to date.");
|
|
7914
|
+
return;
|
|
7915
|
+
}
|
|
7916
|
+
if (dryRun) {
|
|
7917
|
+
console.log(" Would update .codebyplan/server.local.json (dry-run).");
|
|
7918
|
+
return;
|
|
7919
|
+
}
|
|
7920
|
+
await mkdir8(codebyplanDir, { recursive: true });
|
|
7921
|
+
await writeFile14(filePath, newJson, "utf-8");
|
|
7922
|
+
console.log(
|
|
7923
|
+
` Updated .codebyplan/server.local.json (worktree ${resolvedWorktreeId ?? "\u2014"}, ${portAllocations.length} allocation${portAllocations.length === 1 ? "" : "s"}).`
|
|
7924
|
+
);
|
|
7925
|
+
}
|
|
7926
|
+
async function provisionE2eEnv(projectPath, dryRun) {
|
|
7927
|
+
const relSource = join26("apps", "web", ".env.local");
|
|
7928
|
+
const sourcePath = join26(projectPath, relSource);
|
|
7929
|
+
let sourceRaw;
|
|
7930
|
+
try {
|
|
7931
|
+
sourceRaw = await readFile19(sourcePath, "utf-8");
|
|
7932
|
+
} catch {
|
|
7933
|
+
console.warn(
|
|
7934
|
+
` Skipped .codebyplan/e2e.env \u2014 source ${relSource} not found.`
|
|
7935
|
+
);
|
|
7936
|
+
return;
|
|
7937
|
+
}
|
|
7938
|
+
const sourceVars = parseEnvFile(sourceRaw);
|
|
7939
|
+
const lines = [];
|
|
7940
|
+
const missing = [];
|
|
7941
|
+
for (const key of E2E_ENV_VARS) {
|
|
7942
|
+
const val = sourceVars[key];
|
|
7943
|
+
if (val === void 0) {
|
|
7944
|
+
missing.push(key);
|
|
7945
|
+
continue;
|
|
7946
|
+
}
|
|
7947
|
+
lines.push(`${key}=${val}`);
|
|
7948
|
+
}
|
|
7949
|
+
if (missing.length > 0) {
|
|
7950
|
+
console.warn(
|
|
7951
|
+
` Warning: ${missing.length} E2E var(s) missing from ${relSource}: ${missing.join(", ")}`
|
|
7952
|
+
);
|
|
7953
|
+
}
|
|
7954
|
+
if (lines.length === 0) {
|
|
7955
|
+
console.warn(
|
|
7956
|
+
" Skipped .codebyplan/e2e.env \u2014 none of the expected E2E vars were found."
|
|
7957
|
+
);
|
|
7958
|
+
return;
|
|
7959
|
+
}
|
|
7960
|
+
const codebyplanDir = join26(projectPath, ".codebyplan");
|
|
7961
|
+
const filePath = join26(codebyplanDir, "e2e.env");
|
|
7962
|
+
const newContent = lines.join("\n") + "\n";
|
|
7963
|
+
let currentContent = "";
|
|
7964
|
+
try {
|
|
7965
|
+
currentContent = await readFile19(filePath, "utf-8");
|
|
7966
|
+
} catch {
|
|
7967
|
+
}
|
|
7968
|
+
if (currentContent === newContent) {
|
|
7969
|
+
console.log(" e2e.env up to date.");
|
|
7970
|
+
return;
|
|
7971
|
+
}
|
|
7972
|
+
if (dryRun) {
|
|
7973
|
+
console.log(" Would provision .codebyplan/e2e.env (dry-run).");
|
|
7974
|
+
return;
|
|
7975
|
+
}
|
|
7976
|
+
await mkdir8(codebyplanDir, { recursive: true });
|
|
7977
|
+
await writeFile14(filePath, newContent, "utf-8");
|
|
7978
|
+
console.log(
|
|
7979
|
+
` Provisioned .codebyplan/e2e.env (${lines.length} var${lines.length === 1 ? "" : "s"}).`
|
|
7980
|
+
);
|
|
7981
|
+
}
|
|
7982
|
+
function parseEnvFile(raw) {
|
|
7983
|
+
const out = {};
|
|
7984
|
+
for (const line of raw.split("\n")) {
|
|
7985
|
+
const trimmed = line.trim();
|
|
7986
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
7987
|
+
const eq = trimmed.indexOf("=");
|
|
7988
|
+
if (eq === -1) continue;
|
|
7989
|
+
const key = trimmed.slice(0, eq).trim();
|
|
7990
|
+
if (key === "") continue;
|
|
7991
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
7992
|
+
const quoted = value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"));
|
|
7993
|
+
if (quoted) {
|
|
7994
|
+
value = value.slice(1, -1);
|
|
7995
|
+
} else {
|
|
7996
|
+
const commentIdx = value.indexOf(" #");
|
|
7997
|
+
if (commentIdx !== -1) value = value.slice(0, commentIdx).trimEnd();
|
|
7998
|
+
}
|
|
7999
|
+
out[key] = value;
|
|
8000
|
+
}
|
|
8001
|
+
return out;
|
|
8002
|
+
}
|
|
8003
|
+
var E2E_ENV_VARS;
|
|
7825
8004
|
var init_ports = __esm({
|
|
7826
8005
|
"src/cli/ports.ts"() {
|
|
7827
8006
|
"use strict";
|
|
7828
8007
|
init_flags();
|
|
7829
8008
|
init_api();
|
|
7830
8009
|
init_port_verify();
|
|
8010
|
+
init_worktree_port_resolver();
|
|
8011
|
+
E2E_ENV_VARS = [
|
|
8012
|
+
"E2E_USER_EMAIL",
|
|
8013
|
+
"E2E_USER_PASSWORD",
|
|
8014
|
+
"NEXT_PUBLIC_SUPABASE_URL",
|
|
8015
|
+
"NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY",
|
|
8016
|
+
"SUPABASE_SECRET_KEY"
|
|
8017
|
+
];
|
|
7831
8018
|
}
|
|
7832
8019
|
});
|
|
7833
8020
|
|
|
@@ -7845,7 +8032,7 @@ async function runTechStack() {
|
|
|
7845
8032
|
await runFullTechStack(dryRun);
|
|
7846
8033
|
return;
|
|
7847
8034
|
}
|
|
7848
|
-
|
|
8035
|
+
await validateAuth();
|
|
7849
8036
|
const config = await resolveConfig(flags);
|
|
7850
8037
|
const { repoId, projectPath } = config;
|
|
7851
8038
|
console.log(`
|
|
@@ -7980,7 +8167,7 @@ async function syncTechStackForPath(repoId, projectPath, dryRun) {
|
|
|
7980
8167
|
}
|
|
7981
8168
|
}
|
|
7982
8169
|
async function runFullTechStack(dryRun) {
|
|
7983
|
-
|
|
8170
|
+
await validateAuth();
|
|
7984
8171
|
const localConfig = await readLocalConfig(process.cwd());
|
|
7985
8172
|
if (!localConfig?.device_id) {
|
|
7986
8173
|
console.error(
|
|
@@ -8981,6 +9168,11 @@ void (async () => {
|
|
|
8981
9168
|
--repo-id <uuid> Repository ID (or set via .codebyplan/repo.json)
|
|
8982
9169
|
--dry-run Preview changes without writing
|
|
8983
9170
|
--fix Auto-create missing port allocations
|
|
9171
|
+
--write-local Write this worktree's port allocations to the
|
|
9172
|
+
gitignored .codebyplan/server.local.json overlay
|
|
9173
|
+
(never the committed server.json)
|
|
9174
|
+
--provision-e2e Copy E2E credentials from apps/web/.env.local into
|
|
9175
|
+
the gitignored .codebyplan/e2e.env (local, no API)
|
|
8984
9176
|
|
|
8985
9177
|
Tech stack options:
|
|
8986
9178
|
--path <dir> Project root directory (default: cwd)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codebyplan",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.17",
|
|
4
4
|
"description": "CLI for CodeByPlan — AI-powered development planning and tracking",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -61,6 +61,6 @@
|
|
|
61
61
|
"prettier": "^3.8.1",
|
|
62
62
|
"typescript": "^5",
|
|
63
63
|
"typescript-eslint": "^8.20.0",
|
|
64
|
-
"vitest": "^4.1.
|
|
64
|
+
"vitest": "^4.1.8"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -26,136 +26,237 @@ pnpm exec playwright install --with-deps chromium
|
|
|
26
26
|
|
|
27
27
|
## playwright.config.ts
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
Resolve the apps/web dev-server port at config-read time via the shared resolver
|
|
30
|
+
`apps/web/e2e/resolve-web-dev-port.ts` — imported by BOTH `playwright.config.ts` and
|
|
31
|
+
`e2e/auth.setup.ts` (single source of truth). It reads the per-worktree
|
|
32
|
+
`.codebyplan/server.local.json` overlay first, then the committed `.codebyplan/server.json`.
|
|
33
|
+
Match by label rather than array position — a monorepo can have several Next.js allocations
|
|
34
|
+
with similar label prefixes.
|
|
35
|
+
|
|
36
|
+
**Label-matching rules** (`findWebDevPort`):
|
|
37
|
+
|
|
38
|
+
- `server.local.json` overlay: each label has the worktree name appended as the last
|
|
39
|
+
parenthetical group (e.g. `"Web Dev (codebyplan-mcp-1)"`). Strip exactly ONE trailing
|
|
40
|
+
`" (…)"` group, then require the result `=== "Web Dev"`.
|
|
41
|
+
- `"Web Dev (codebyplan-mcp-1)"` → strip → `"Web Dev"` ✓
|
|
42
|
+
- `"Web Dev (codebyplan-desktop) (codebyplan-mcp-1)"` → strip → `"Web Dev (codebyplan-desktop)"` ✗
|
|
43
|
+
- `server.json` committed base: require `label === "Web Dev"` exactly (do NOT strip —
|
|
44
|
+
`"Web Dev (codebyplan-desktop)"` must not match).
|
|
45
|
+
|
|
46
|
+
**Resolution order** (first hit wins):
|
|
47
|
+
|
|
48
|
+
0. `PLAYWRIGHT_BASE_URL` — explicit CI / local override (`parsePortFromUrl` extracts the port)
|
|
49
|
+
1. `.codebyplan/server.local.json` — `findWebDevPort(…, {stripWorktreeSuffix: true})`
|
|
50
|
+
2. `.codebyplan/server.json` — `findWebDevPort(…, {stripWorktreeSuffix: false})`
|
|
51
|
+
3. `E2E_BASE_URL` — `parsePortFromUrl` (kept BELOW the overlay: a stale `E2E_BASE_URL` in a
|
|
52
|
+
gitignored `.env.local` must never shadow the worktree's own port — set `PLAYWRIGHT_BASE_URL`
|
|
53
|
+
to override in CI)
|
|
54
|
+
4. `3010` — last resort
|
|
55
|
+
|
|
56
|
+
The resolver uses `readFileSync` + `JSON.parse` with paths relative to `apps/web/e2e/`
|
|
57
|
+
(`resolve(__dirname, "../../../.codebyplan/…")`). Each read is wrapped in `try/catch` — the
|
|
58
|
+
overlay is gitignored and absent in CI. Do NOT import from the `codebyplan` CLI package
|
|
59
|
+
(async, cross-package coupling). `findWebDevPort` + `parsePortFromUrl` are pure and unit-tested
|
|
60
|
+
in `e2e/__tests__/resolve-web-dev-port.test.ts`.
|
|
31
61
|
|
|
32
62
|
```ts
|
|
63
|
+
import { readFileSync } from "node:fs";
|
|
64
|
+
import { resolve } from "node:path";
|
|
33
65
|
import { defineConfig, devices } from "@playwright/test";
|
|
34
|
-
import { execSync } from "child_process";
|
|
35
66
|
|
|
36
|
-
|
|
67
|
+
import { resolveWebDevPort } from "./e2e/resolve-web-dev-port";
|
|
68
|
+
|
|
69
|
+
// Load apps/web/.env.local into process.env (process.env wins on conflict)
|
|
70
|
+
(function loadDotEnvLocal() {
|
|
71
|
+
try {
|
|
72
|
+
const text = readFileSync(resolve(__dirname, ".env.local"), "utf-8");
|
|
73
|
+
for (const line of text.split("\n")) {
|
|
74
|
+
const t = line.trim();
|
|
75
|
+
if (!t || t.startsWith("#")) continue;
|
|
76
|
+
const eq = t.indexOf("=");
|
|
77
|
+
if (eq === -1) continue;
|
|
78
|
+
const k = t.slice(0, eq).trim();
|
|
79
|
+
let v = t.slice(eq + 1).trim();
|
|
80
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")))
|
|
81
|
+
v = v.slice(1, -1);
|
|
82
|
+
if (!(k in process.env)) process.env[k] = v;
|
|
83
|
+
}
|
|
84
|
+
} catch { /* absent in CI */ }
|
|
85
|
+
})();
|
|
86
|
+
|
|
87
|
+
// Load .codebyplan/e2e.env — Supabase + auth credentials (process.env wins)
|
|
88
|
+
(function loadE2eEnv() {
|
|
37
89
|
try {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
90
|
+
const text = readFileSync(resolve(__dirname, "../../.codebyplan/e2e.env"), "utf-8");
|
|
91
|
+
for (const line of text.split("\n")) {
|
|
92
|
+
const t = line.trim();
|
|
93
|
+
if (!t || t.startsWith("#")) continue;
|
|
94
|
+
const eq = t.indexOf("=");
|
|
95
|
+
if (eq === -1) continue;
|
|
96
|
+
const k = t.slice(0, eq).trim();
|
|
97
|
+
let v = t.slice(eq + 1).trim();
|
|
98
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")))
|
|
99
|
+
v = v.slice(1, -1);
|
|
100
|
+
if (!(k in process.env)) process.env[k] = v;
|
|
101
|
+
}
|
|
102
|
+
} catch { /* absent in CI — shell / CI secrets used instead */ }
|
|
103
|
+
})();
|
|
104
|
+
|
|
105
|
+
// findWebDevPort, parsePortFromUrl, and resolveWebDevPort live in the shared
|
|
106
|
+
// module ./e2e/resolve-web-dev-port.ts (imported above) — single source of
|
|
107
|
+
// truth, also consumed by e2e/auth.setup.ts. Resolution order:
|
|
108
|
+
// 0. PLAYWRIGHT_BASE_URL → 1. server.local.json → 2. server.json
|
|
109
|
+
// → 3. E2E_BASE_URL → 4. 3010.
|
|
110
|
+
const port = resolveWebDevPort();
|
|
48
111
|
|
|
49
112
|
export default defineConfig({
|
|
50
|
-
testDir: "
|
|
51
|
-
|
|
113
|
+
testDir: "./e2e",
|
|
114
|
+
testMatch: "*.spec.ts",
|
|
115
|
+
globalSetup: require.resolve("./e2e/global-setup"),
|
|
116
|
+
fullyParallel: true,
|
|
52
117
|
forbidOnly: !!process.env.CI,
|
|
53
|
-
retries: process.env.CI ?
|
|
118
|
+
retries: process.env.CI ? 1 : 0,
|
|
54
119
|
workers: 1, // serialize against shared remote Supabase — see e2e.md § Supabase Parallelism
|
|
55
|
-
reporter:
|
|
56
|
-
|
|
120
|
+
reporter: "list",
|
|
121
|
+
timeout: 30_000,
|
|
122
|
+
expect: {
|
|
123
|
+
toHaveScreenshot: { stylePath: "./e2e/screenshot.css" },
|
|
124
|
+
},
|
|
57
125
|
use: {
|
|
58
|
-
baseURL:
|
|
126
|
+
baseURL: `http://localhost:${port}`,
|
|
127
|
+
storageState: "e2e/.auth/refreshed-state.json",
|
|
128
|
+
actionTimeout: 15_000,
|
|
59
129
|
trace: "on-first-retry",
|
|
60
130
|
screenshot: "only-on-failure",
|
|
61
131
|
},
|
|
62
|
-
projects: [
|
|
63
|
-
{ name: "setup", testMatch: /global\.setup\.ts/ },
|
|
64
|
-
{
|
|
65
|
-
name: "web",
|
|
66
|
-
use: { ...devices["Desktop Chrome"], storageState: "apps/web/e2e/.auth/user.json" },
|
|
67
|
-
dependencies: ["setup"],
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
132
|
webServer: {
|
|
71
|
-
command:
|
|
72
|
-
url:
|
|
133
|
+
command: `pnpm --filter @codebyplan/web dev --port ${port}`,
|
|
134
|
+
url: `http://localhost:${port}`,
|
|
73
135
|
reuseExistingServer: !process.env.CI,
|
|
74
136
|
timeout: 120_000,
|
|
137
|
+
env: {
|
|
138
|
+
// Forward Supabase + auth vars into the spawned dev server. Only forward
|
|
139
|
+
// vars that are present in process.env (undefined would stringify to "undefined").
|
|
140
|
+
...(process.env.NEXT_PUBLIC_SUPABASE_URL && {
|
|
141
|
+
NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
142
|
+
}),
|
|
143
|
+
...(process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY && {
|
|
144
|
+
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,
|
|
145
|
+
}),
|
|
146
|
+
...(process.env.SUPABASE_SECRET_KEY && {
|
|
147
|
+
SUPABASE_SECRET_KEY: process.env.SUPABASE_SECRET_KEY,
|
|
148
|
+
}),
|
|
149
|
+
},
|
|
75
150
|
},
|
|
151
|
+
projects: [
|
|
152
|
+
{
|
|
153
|
+
name: "chromium",
|
|
154
|
+
use: { ...devices["Desktop Chrome"] },
|
|
155
|
+
},
|
|
156
|
+
],
|
|
76
157
|
});
|
|
77
158
|
```
|
|
78
159
|
|
|
79
160
|
## Auth — Global Setup + Storage State
|
|
80
161
|
|
|
81
|
-
`apps/web/e2e/global-setup.ts
|
|
162
|
+
`apps/web/e2e/global-setup.ts` performs two phases at startup:
|
|
82
163
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
export default async function globalSetup(config: FullConfig) {
|
|
90
|
-
const email = process.env.E2E_TEST_EMAIL;
|
|
91
|
-
const password = process.env.E2E_TEST_PASSWORD;
|
|
92
|
-
|
|
93
|
-
if (!email || !password) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
"E2E_TEST_EMAIL and E2E_TEST_PASSWORD must be set.\n" +
|
|
96
|
-
"Copy .env.local.example to .env.local, then run: pnpm e2e:provision"
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const { baseURL } = config.projects[0].use;
|
|
101
|
-
const browser = await chromium.launch();
|
|
102
|
-
const page = await browser.newPage();
|
|
103
|
-
|
|
104
|
-
await page.goto(`${baseURL}/login`);
|
|
105
|
-
await page.getByLabel(/email/i).fill(email);
|
|
106
|
-
await page.getByLabel(/password/i).fill(password);
|
|
107
|
-
await page.getByRole("button", { name: /sign in|log in/i }).click();
|
|
108
|
-
await page.waitForURL(/\/(dashboard|home|app)/, { timeout: 15_000 });
|
|
109
|
-
|
|
110
|
-
await page.goto(baseURL!); // cold-start warmup
|
|
111
|
-
await page.context().storageState({ path: AUTH_FILE });
|
|
112
|
-
await browser.close();
|
|
113
|
-
}
|
|
114
|
-
```
|
|
164
|
+
**Phase 1 — Auth refresh**: reads `e2e/.auth/state.json`, finds the Supabase auth cookie
|
|
165
|
+
(`sb-<projectref>-auth-token`), decodes its base64-JSON payload (`decodeAuthCookie` from the
|
|
166
|
+
shared `e2e/auth-cookie.ts` module), calls `supabase.auth.refreshSession({refresh_token})` for
|
|
167
|
+
fresh tokens, re-encodes via `encodeAuthCookie`, and writes the result to
|
|
168
|
+
`e2e/.auth/refreshed-state.json`. No browser required — pure HTTP against Supabase auth.
|
|
115
169
|
|
|
116
|
-
|
|
170
|
+
**Phase 2 — Maintainer seeding**: uses the service-role client to ensure the test user
|
|
171
|
+
holds a maintainer-or-above role on at least one organization (`e2e-test-fixture` slug).
|
|
172
|
+
Idempotent — if a qualifying membership already exists, phase 2 is a no-op.
|
|
117
173
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
174
|
+
**Required env vars** (read via `readEnv(name, fallbacks)` which checks `process.env` first,
|
|
175
|
+
then falls back to the parsed `.env.local` file):
|
|
176
|
+
|
|
177
|
+
- `NEXT_PUBLIC_SUPABASE_URL` — both phases
|
|
178
|
+
- `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` — Phase 1 refresh client
|
|
179
|
+
- `SUPABASE_SECRET_KEY` — Phase 2 admin operations
|
|
180
|
+
- `E2E_USER_EMAIL` — Phase 2 user lookup + seeding
|
|
181
|
+
|
|
182
|
+
Because `readEnv` checks `process.env` before `.env.local`, loading these vars into
|
|
183
|
+
`process.env` via `loadE2eEnv()` in `playwright.config.ts` is sufficient — global-setup
|
|
184
|
+
will pick them up.
|
|
185
|
+
|
|
186
|
+
### Seeding `state.json` — `e2e/auth.setup.ts` (`pnpm e2e:auth-setup`)
|
|
187
|
+
|
|
188
|
+
global-setup Phase 1 only *refreshes* an existing `state.json`; the initial seed is written by
|
|
189
|
+
`e2e/auth.setup.ts`. It is a pure-HTTP API seed — **no browser, no dev server, no hydration
|
|
190
|
+
timing**: it loads creds from `.env.local` + `.codebyplan/e2e.env`, calls
|
|
191
|
+
`supabase.auth.signInWithPassword({email, password})` with the publishable-key client, derives
|
|
192
|
+
the project ref from `NEXT_PUBLIC_SUPABASE_URL`, and writes a `sb-<projectref>-auth-token`
|
|
193
|
+
cookie (domain `localhost`) into `state.json` using the same `encodeAuthCookie` from
|
|
194
|
+
`e2e/auth-cookie.ts` that global-setup consumes. This makes seeding deterministic in any
|
|
195
|
+
worktree — run `pnpm e2e:auth-setup` (optionally `--port N`) when `state.json` is missing or
|
|
196
|
+
its refresh token has expired. Do NOT reintroduce a browser-login flow (the `(auth)/login`
|
|
197
|
+
page is a client component whose `onSubmit` only attaches after hydration — clicking submit
|
|
198
|
+
pre-hydration falls through to a native GET and never authenticates).
|
|
122
199
|
|
|
123
200
|
## Auth Probe
|
|
124
201
|
|
|
125
|
-
`apps/web/e2e/_probe/auth.spec.ts` —
|
|
126
|
-
state
|
|
202
|
+
`apps/web/e2e/_probe/auth.spec.ts` — verifies that the stored auth state
|
|
203
|
+
(`refreshed-state.json`) grants access to the authenticated dashboard without
|
|
204
|
+
redirecting to the login page. It is intentionally minimal (one test) and runs
|
|
205
|
+
before the full suite to confirm the auth preflight:
|
|
127
206
|
|
|
128
207
|
```ts
|
|
129
208
|
import { test, expect } from "@playwright/test";
|
|
130
209
|
|
|
131
|
-
test("auth probe:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
await page.getByLabel(/email/i).fill(email!);
|
|
139
|
-
await page.getByLabel(/password/i).fill(password!);
|
|
140
|
-
await page.getByRole("button", { name: /sign in|log in/i }).click();
|
|
210
|
+
test("auth probe: authenticated user reaches /dashboard without login redirect", async ({
|
|
211
|
+
page,
|
|
212
|
+
}) => {
|
|
213
|
+
const response = await page.goto("/dashboard", {
|
|
214
|
+
waitUntil: "domcontentloaded",
|
|
215
|
+
timeout: 20_000,
|
|
216
|
+
});
|
|
141
217
|
|
|
142
|
-
|
|
218
|
+
// Must NOT be redirected to /login or /auth/login.
|
|
219
|
+
const finalUrl = page.url();
|
|
220
|
+
expect(
|
|
221
|
+
finalUrl,
|
|
222
|
+
`Auth probe failed: landed on ${finalUrl} instead of /dashboard. Check state.json / refreshed-state.json.`
|
|
223
|
+
).not.toMatch(/\/(auth\/)?login/);
|
|
224
|
+
|
|
225
|
+
// HTTP status must be < 400.
|
|
226
|
+
const status = response?.status() ?? 0;
|
|
227
|
+
expect(
|
|
228
|
+
status,
|
|
229
|
+
`Auth probe failed: /dashboard returned HTTP ${status}.`
|
|
230
|
+
).toBeLessThan(400);
|
|
231
|
+
|
|
232
|
+
// Dashboard heading must be present.
|
|
233
|
+
await expect(
|
|
234
|
+
page.getByRole("heading", { level: 1, name: /welcome to codebyplan|dashboard/i })
|
|
235
|
+
).toBeVisible({ timeout: 15_000 });
|
|
143
236
|
});
|
|
144
237
|
```
|
|
145
238
|
|
|
146
|
-
Run probe: `pnpm exec playwright test --project=
|
|
239
|
+
Run probe: `pnpm exec playwright test --project=chromium _probe/auth`
|
|
147
240
|
|
|
148
241
|
## Pre-flight Probes (Step 6.5.2)
|
|
149
242
|
|
|
150
243
|
**Dev server**: `curl -s -o /dev/null -w "%{http_code}" http://localhost:{port}/` — expect
|
|
151
244
|
200/3xx. On failure:
|
|
152
245
|
|
|
153
|
-
> "Dev server is not responding on port `{port}`. Please run `cd apps/
|
|
246
|
+
> "Dev server is not responding on port `{port}`. Please run `cd apps/web && pnpm dev --port {port}`
|
|
154
247
|
> in a separate terminal, then reply 'ready' when the page loads in your browser."
|
|
155
248
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
249
|
+
Note: Playwright's `webServer` block behaviour differs by environment. In local worktree
|
|
250
|
+
runs (`reuseExistingServer: true`), Playwright reuses an already-running server and only
|
|
251
|
+
auto-starts one when nothing is listening on the port — this probe is mainly a safety net.
|
|
252
|
+
In CI (`reuseExistingServer: false`), Playwright always spawns a fresh server regardless of
|
|
253
|
+
any already-running process, so the dev-server readiness probe is the active guard for that
|
|
254
|
+
path.
|
|
255
|
+
|
|
256
|
+
**Port alignment**: parse `playwright.config.ts` `baseURL` port; compare to the resolved
|
|
257
|
+
port from `.codebyplan/server.local.json` (worktree overlay, checked first) then
|
|
258
|
+
`.codebyplan/server.json` (committed base). On mismatch ask which is correct, then propose
|
|
259
|
+
an Edit to align them.
|
|
159
260
|
|
|
160
261
|
## Spec-Writing Patterns
|
|
161
262
|
|
|
@@ -227,7 +328,7 @@ Include this in the specialist output alongside `screenshots[]`.
|
|
|
227
328
|
## Run Command
|
|
228
329
|
|
|
229
330
|
```bash
|
|
230
|
-
pnpm exec playwright test {spec} --project=
|
|
331
|
+
pnpm exec playwright test {spec} --project=chromium --reporter=list
|
|
231
332
|
```
|
|
232
333
|
|
|
233
334
|
## Selector Conventions
|
|
@@ -238,13 +339,13 @@ the new page state rather than holding stale `Locator` handles.
|
|
|
238
339
|
|
|
239
340
|
## CI Secrets
|
|
240
341
|
|
|
241
|
-
`
|
|
242
|
-
`NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
|
|
342
|
+
`E2E_USER_EMAIL`, `E2E_USER_PASSWORD`, `NEXT_PUBLIC_SUPABASE_URL`,
|
|
343
|
+
`NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`, `SUPABASE_SECRET_KEY`.
|
|
243
344
|
|
|
244
345
|
## Pitfalls
|
|
245
346
|
|
|
246
347
|
**Cold-start timeouts** — warmup in `globalSetup` (after `page.goto(baseURL!)`) primes
|
|
247
|
-
Turbopack compilation. **Port mismatch** — compare `baseURL` port to
|
|
248
|
-
running. **Supabase parallelism** — remote
|
|
249
|
-
auth/RLS races. **SCSS Module selectors** — use
|
|
250
|
-
role-based selectors.
|
|
348
|
+
Turbopack compilation. **Port mismatch** — compare `baseURL` port to resolved port from
|
|
349
|
+
`server.local.json` / `server.json` before running. **Supabase parallelism** — remote
|
|
350
|
+
Supabase requires `workers: 1` to prevent auth/RLS races. **SCSS Module selectors** — use
|
|
351
|
+
`[class*='componentName'].first()` or role-based selectors.
|
|
@@ -118,11 +118,12 @@ with the blocking preflight field populated.
|
|
|
118
118
|
|
|
119
119
|
### 6.5.1 Environment Variables
|
|
120
120
|
|
|
121
|
-
Check `apps/{app}/.env.local
|
|
122
|
-
from the `credential_vars` input field (the dispatching skill reads
|
|
123
|
-
`.codebyplan/e2e.json`). Naming conventions:
|
|
121
|
+
Check `apps/{app}/.env.local`, `.codebyplan/e2e.env`, and process env. Framework-specific
|
|
122
|
+
required var names come from the `credential_vars` input field (the dispatching skill reads
|
|
123
|
+
them from `.codebyplan/e2e.json`). Naming conventions:
|
|
124
124
|
|
|
125
|
-
- Playwright uses `
|
|
125
|
+
- Playwright uses `E2E_USER_EMAIL` / `E2E_USER_PASSWORD` (matches `.codebyplan/e2e.json`
|
|
126
|
+
`credentials.frameworks.playwright` and the `e2e.env` file loaded by `playwright.config.ts`).
|
|
126
127
|
- Maestro/XCUITest stay on `TEST_*` per `rules/maestro-auth-state-reset.md`.
|
|
127
128
|
|
|
128
129
|
For any missing var:
|
|
@@ -148,8 +149,9 @@ On any failure, `AskUserQuestion` with remediation steps; re-probe after "ready"
|
|
|
148
149
|
silently skip a required runtime prerequisite.
|
|
149
150
|
|
|
150
151
|
**Port alignment (Playwright only)**: parse `playwright.config.ts` `baseURL` and compare
|
|
151
|
-
to `.codebyplan/server.json`
|
|
152
|
-
|
|
152
|
+
to the resolved port from `.codebyplan/server.local.json` (worktree overlay, checked first)
|
|
153
|
+
then `.codebyplan/server.json` (committed base) `port_allocations[]` for the app. On
|
|
154
|
+
mismatch ask which is correct before running.
|
|
153
155
|
|
|
154
156
|
### 6.5.3 Auth Probe (only when `has_auth`)
|
|
155
157
|
|
|
@@ -303,9 +305,12 @@ Every repo with Playwright auth ships:
|
|
|
303
305
|
|
|
304
306
|
- `scripts/provision-e2e-user.ts` — idempotent script creating the canonical E2E user
|
|
305
307
|
and (for multi-tenant repos) a `test` subdomain. Wired to `pnpm e2e:provision`.
|
|
306
|
-
- `.env
|
|
307
|
-
|
|
308
|
-
`
|
|
308
|
+
- `.codebyplan/e2e.env` — gitignored per-worktree file listing every env var `globalSetup`
|
|
309
|
+
requires. Written by `codebyplan e2e:provision` or manually. Loaded by `playwright.config.ts`
|
|
310
|
+
into `process.env` so global-setup picks them up via its `readEnv` helper.
|
|
311
|
+
- `.env.local.example` — lists every env var `globalSetup` requires for reference.
|
|
312
|
+
- CI secrets: `E2E_USER_EMAIL`, `E2E_USER_PASSWORD`, `NEXT_PUBLIC_SUPABASE_URL`,
|
|
313
|
+
`NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`, `SUPABASE_SECRET_KEY`.
|
|
309
314
|
|
|
310
315
|
Per-repo specifics (email, vault name, remaining-spec migration list) live in the repo's
|
|
311
316
|
own `docs/e2e-setup.md`, not in this shared file.
|
|
@@ -142,6 +142,17 @@ cp "$MAIN_REPO/.env.local" "$WORKTREE_PATH/.env.local"
|
|
|
142
142
|
|
|
143
143
|
Verify `.env.local` is already in `.gitignore` (it should be via `.env.local` pattern). If not, add it.
|
|
144
144
|
|
|
145
|
+
Also copy the gitignored E2E credentials source (`.codebyplan/e2e.env`, referenced by `.codebyplan/e2e.json`) so the new worktree can run Playwright auth flows immediately:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
mkdir -p "$WORKTREE_PATH/.codebyplan"
|
|
149
|
+
if [ -f "$MAIN_REPO/.codebyplan/e2e.env" ]; then
|
|
150
|
+
cp "$MAIN_REPO/.codebyplan/e2e.env" "$WORKTREE_PATH/.codebyplan/e2e.env"
|
|
151
|
+
fi
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
If the main repo has no `.codebyplan/e2e.env` yet, provision it after setup by running `codebyplan ports --path "$WORKTREE_PATH" --provision-e2e` (copies the canonical E2E vars from `apps/web/.env.local`). Pass `--path` BEFORE the boolean flag. `.codebyplan/e2e.env` is gitignored — never commit it.
|
|
155
|
+
|
|
145
156
|
### Step 8: Push Branch
|
|
146
157
|
|
|
147
158
|
```bash
|