@timekast/factory 2.0.1 → 2.2.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/dist/commands/add.js +4 -3
- package/dist/commands/provision.js +36 -14
- package/dist/commands/ticket.js +17 -48
- package/dist/commands/update.js +4 -3
- package/dist/commands/vault.js +39 -5
- package/dist/index.js +2 -2
- package/dist/lib/claude-md.js +8 -3
- package/dist/lib/email-from.js +30 -0
- package/dist/lib/repo-detection.js +42 -1
- package/package.json +1 -1
package/dist/commands/add.js
CHANGED
|
@@ -38,7 +38,7 @@ import { CLAUDE_MD_FILE, PROFILES } from '../lib/constants.js';
|
|
|
38
38
|
import { hasLockfile, writeInitialLockfile } from '../lib/lockfile.js';
|
|
39
39
|
import { detectProfile, maintainKitScripts } from '../lib/package-json.js';
|
|
40
40
|
import { runPreflight } from '../lib/preflight.js';
|
|
41
|
-
import { detectRepo } from '../lib/repo-detection.js';
|
|
41
|
+
import { detectRepo, resolveBrainRoot } from '../lib/repo-detection.js';
|
|
42
42
|
import { downloadProfileTarball, stageProfileTarball } from '../lib/unpack.js';
|
|
43
43
|
import { reportPkgMutations, runUpdate } from './update.js';
|
|
44
44
|
/** Message shown when `add` runs outside any git repo context. */
|
|
@@ -56,11 +56,12 @@ export async function runAdd(flags = { core: false, full: false }) {
|
|
|
56
56
|
// network call or write — nothing is touched when there is no repo. The
|
|
57
57
|
// brain lives at the REPO ROOT, so all gating + install target `root`, not
|
|
58
58
|
// `cwd` — running `add` from a subdir must not plant a nested second brain.
|
|
59
|
-
const { hasRepo
|
|
59
|
+
const { hasRepo } = detectRepo(cwd);
|
|
60
60
|
if (!hasRepo) {
|
|
61
61
|
throw new CLIError(NO_REPO_MESSAGE);
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
// Same target guard as `update`: never a root that is `~` or contains it.
|
|
64
|
+
const root = resolveBrainRoot(cwd);
|
|
64
65
|
// 2. Gate on a pre-existing brain (checked at the repo root, not cwd). `add` is
|
|
65
66
|
// for a repo WITHOUT one; anything else routes to `update` so the raw
|
|
66
67
|
// `applyPlan` install never clobbers an old `.claude/` (leaving orphans, no
|
|
@@ -30,6 +30,7 @@ import { execa } from 'execa';
|
|
|
30
30
|
import prompts from 'prompts';
|
|
31
31
|
import { FACTORY_ORG } from '../lib/constants.js';
|
|
32
32
|
import { CLIError } from '../lib/cli-error.js';
|
|
33
|
+
import { sendingDomainOf } from '../lib/email-from.js';
|
|
33
34
|
import { apexZoneOf, CloudflareApiError, CLOUDFLARE_API_BASE, absoluteRecordName, deleteDnsRecord, getDnsRecord, ensureCnameRecord, ensureDnsRecord, ensureUnproxiedRecord, resolveZoneForDomain, findCnameRecord, resolveZoneId, resolveZoneIdForName, resolveZoneIdForDomain, CNAME_VERCEL_TARGET, TIMEKAST_ZONE_NAME, provisionDomains, existingProvisionDomains, ORG_ZONE_NAMES, } from '../lib/cloudflare-api.js';
|
|
34
35
|
import { createDevelopBranch, createNeonProject, createProjectApiKey, deleteNeonApiKeysByName, getNeonProjectOrg, listNeonOrgs, resolveNeonOrg, deleteNeonProject, getConnectionUri, isNeonKeyFamily, listNeonApiKeys, newestNeonFamilyKey, listNeonBranches, listNeonProjects, NeonApiError, NeonApiKeyListError, revokeNeonApiKey, rotatedNeonKeyName, NEON_API_BASE, NEON_DEVELOP_BRANCH, } from '../lib/neon-api.js';
|
|
35
36
|
import { createBacklogProject, deleteBacklogProject, mintBacklogKey, BacklogApiError, } from '../lib/backlog-api.js';
|
|
@@ -1928,9 +1929,7 @@ const RESEND_KEY_RESUME_HINT = 'repite la corrida con `--resume` y `dns` en `--s
|
|
|
1928
1929
|
*/
|
|
1929
1930
|
function noVaultMintDomain(state) {
|
|
1930
1931
|
const client = state.dns?.clientDomainStatus === 'live' ? state.dns.clientDomain : undefined;
|
|
1931
|
-
return client
|
|
1932
|
-
? mailDomainFor(client)
|
|
1933
|
-
: PROVISION_EMAIL_FROM.slice(PROVISION_EMAIL_FROM.indexOf('@') + 1);
|
|
1932
|
+
return client ? mailDomainFor(client) : sendingDomainOf(PROVISION_EMAIL_FROM);
|
|
1934
1933
|
}
|
|
1935
1934
|
/**
|
|
1936
1935
|
* NO-vault path (F9 — the rail's team key never reaches a deploy): the project's OWN send-only
|
|
@@ -2029,7 +2028,7 @@ async function ensureProjectResendKey(rootDir, state, ctx, rail, fetchImpl, pers
|
|
|
2029
2028
|
return null;
|
|
2030
2029
|
}
|
|
2031
2030
|
const sender = vaultSender(main);
|
|
2032
|
-
const domain =
|
|
2031
|
+
const domain = sendingDomainOf(sender);
|
|
2033
2032
|
// SYNC-001: a key this one REPLACES is revoked only once the syncs delivered the new one — the
|
|
2034
2033
|
// marker is each deploy sync's last job (Vercel or Railway) before the write (taken only when there is a key to
|
|
2035
2034
|
// revoke later). Taken BEFORE the mint: a vault that cannot list its syncs stops the run here,
|
|
@@ -7545,8 +7544,19 @@ export const RAILWAY_ENVIRONMENTS = [
|
|
|
7545
7544
|
];
|
|
7546
7545
|
/** The port the app listens on in Railway: the vault writes it as `PORT` (Decision 10). */
|
|
7547
7546
|
export const RAILWAY_APP_PORT = 3000;
|
|
7548
|
-
/**
|
|
7549
|
-
|
|
7547
|
+
/**
|
|
7548
|
+
* The public liveness route the probe asks, and the service's `healthcheckPath` once it answers.
|
|
7549
|
+
* It touches no database: Railway only asks whether the process started, and a public route that
|
|
7550
|
+
* queries Neon lets any bot keep it from autosuspending.
|
|
7551
|
+
*/
|
|
7552
|
+
export const RAILWAY_HEALTH_PATH = '/api/health/live';
|
|
7553
|
+
/**
|
|
7554
|
+
* The fallback for a derivative whose `src/` predates the liveness route but applied the earlier
|
|
7555
|
+
* retrofit, which made the DB-backed `/api/health` public. Asked only when `/api/health/live`
|
|
7556
|
+
* answers 404, so the CLI tolerates that frozen `src/` (BR-FACTORY-006) instead of leaving its
|
|
7557
|
+
* healthcheck off.
|
|
7558
|
+
*/
|
|
7559
|
+
export const RAILWAY_LEGACY_HEALTH_PATH = '/api/health';
|
|
7550
7560
|
/**
|
|
7551
7561
|
* The service's commands, with the vault wrapper OFF explicitly (Decision 11): in Railway the env
|
|
7552
7562
|
* already holds what the sync delivered. Railway runs build → pre-deploy → start, so the build still
|
|
@@ -7830,14 +7840,14 @@ async function waitForRailwayDeployment(client, role, deploymentId, seams) {
|
|
|
7830
7840
|
}
|
|
7831
7841
|
}
|
|
7832
7842
|
/**
|
|
7833
|
-
* One ANONYMOUS request to `/api/health` through Railway's own domain of an
|
|
7834
|
-
* cookie, no token, redirects not followed (a redirect to the login is "not
|
|
7835
|
-
* 401/403/3xx → `not-public`; anything else, or no answer in time → `down`.
|
|
7843
|
+
* One ANONYMOUS request to `path` (default `/api/health/live`) through Railway's own domain of an
|
|
7844
|
+
* environment: no cookie, no token, redirects not followed (a redirect to the login is "not
|
|
7845
|
+
* public"). 200 → `ok`; 401/403/3xx → `not-public`; anything else, or no answer in time → `down`.
|
|
7836
7846
|
*/
|
|
7837
|
-
export async function probeRailwayHealth(domain, fetchImpl) {
|
|
7847
|
+
export async function probeRailwayHealth(domain, fetchImpl, path = RAILWAY_HEALTH_PATH) {
|
|
7838
7848
|
let status;
|
|
7839
7849
|
try {
|
|
7840
|
-
const response = await fetchImpl(`https://${domain}${
|
|
7850
|
+
const response = await fetchImpl(`https://${domain}${path}`, {
|
|
7841
7851
|
method: 'GET',
|
|
7842
7852
|
redirect: 'manual',
|
|
7843
7853
|
signal: AbortSignal.timeout(RAILWAY_PROBE_TIMEOUT_MS),
|
|
@@ -7867,6 +7877,8 @@ function railwayProbeFetch(seams) {
|
|
|
7867
7877
|
* Probe each environment whose healthcheck is not `on` yet; with 200, set `healthcheckPath` there
|
|
7868
7878
|
* and mark it `on`, otherwise mark it `pending` and print the retrofit instruction. Never fails
|
|
7869
7879
|
* the alta: a derivative whose `src/` lacks the public route keeps deploying without healthcheck.
|
|
7880
|
+
* A 404 on the liveness route falls back to the legacy public `/api/health` (see
|
|
7881
|
+
* `RAILWAY_LEGACY_HEALTH_PATH`); with 200 there it is used, and the guide to move off it is named.
|
|
7870
7882
|
*/
|
|
7871
7883
|
async function settleRailwayHealthchecks(client, rw, seams, persist) {
|
|
7872
7884
|
const probeFetch = railwayProbeFetch(seams);
|
|
@@ -7877,15 +7889,25 @@ async function settleRailwayHealthchecks(client, rw, seams, persist) {
|
|
|
7877
7889
|
if (!domain)
|
|
7878
7890
|
continue;
|
|
7879
7891
|
const result = await probeRailwayHealth(domain, probeFetch);
|
|
7880
|
-
|
|
7892
|
+
let path = RAILWAY_HEALTH_PATH;
|
|
7893
|
+
if (result.kind === 'down' && result.status === 404) {
|
|
7894
|
+
const legacy = await probeRailwayHealth(domain, probeFetch, RAILWAY_LEGACY_HEALTH_PATH);
|
|
7895
|
+
if (legacy.kind === 'ok')
|
|
7896
|
+
path = RAILWAY_LEGACY_HEALTH_PATH;
|
|
7897
|
+
}
|
|
7898
|
+
if (result.kind === 'ok' || path === RAILWAY_LEGACY_HEALTH_PATH) {
|
|
7881
7899
|
await client.updateServiceInstance({
|
|
7882
7900
|
serviceId: rw.serviceId,
|
|
7883
7901
|
environmentId: rw.environments?.[role],
|
|
7884
|
-
settings: { healthcheckPath:
|
|
7902
|
+
settings: { healthcheckPath: path },
|
|
7885
7903
|
});
|
|
7886
7904
|
rw.healthcheck = { ...rw.healthcheck, [role]: 'on' };
|
|
7887
7905
|
persist();
|
|
7888
|
-
console.log(` ✔ healthcheck de \`${role}\` activado (\`${
|
|
7906
|
+
console.log(` ✔ healthcheck de \`${role}\` activado (\`${path}\` respondió 200)`);
|
|
7907
|
+
if (path === RAILWAY_LEGACY_HEALTH_PATH) {
|
|
7908
|
+
console.warn(` ℹ Este \`src/\` no tiene \`${RAILWAY_HEALTH_PATH}\`; uso \`${path}\`, que es pública y consulta la base ` +
|
|
7909
|
+
`(cualquier sonda anónima la despierta). Para pasarte a la ruta sin base: §5 de \`${RAILWAY_HEALTH_RETROFIT_GUIDE}\`.`);
|
|
7910
|
+
}
|
|
7889
7911
|
continue;
|
|
7890
7912
|
}
|
|
7891
7913
|
rw.healthcheck = { ...rw.healthcheck, [role]: 'pending' };
|
package/dist/commands/ticket.js
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* A derivative documents a kit defect as a markdown file under `project/factory/`
|
|
5
5
|
* (the `fx-factory-tickets` convention). Until now that file was a dead end: it sat
|
|
6
6
|
* in the repo until somebody opened the Factory by hand. This command delivers it as
|
|
7
|
-
* a GitHub issue in the Factory repo and
|
|
8
|
-
*
|
|
7
|
+
* a GitHub issue in the Factory repo and then DELETES the local file: it was only a
|
|
8
|
+
* draft, and from here on the issue is the one source of state (open / closed,
|
|
9
|
+
* labels). A local copy with its own status only drifted from GitHub, and a second
|
|
10
|
+
* `push` of it opened a duplicate issue (`fx-factory-tickets §6`).
|
|
9
11
|
*
|
|
10
12
|
* THREE THINGS THIS COMMAND DELIBERATELY DOES NOT DO:
|
|
11
13
|
*
|
|
@@ -34,7 +36,7 @@
|
|
|
34
36
|
* preflight degrades to a note + `exit 0` — the same `not-configured` contract
|
|
35
37
|
* `factory backlog push` has without `BACKLOG_API_KEY`, never a stack trace.
|
|
36
38
|
*/
|
|
37
|
-
import { existsSync, readFileSync, realpathSync, statSync,
|
|
39
|
+
import { existsSync, readFileSync, realpathSync, statSync, unlinkSync } from 'node:fs';
|
|
38
40
|
import path from 'node:path';
|
|
39
41
|
import { execa } from 'execa';
|
|
40
42
|
import prompts from 'prompts';
|
|
@@ -163,36 +165,6 @@ export function resolveTicketPath(rootDir, filePath) {
|
|
|
163
165
|
return realTarget;
|
|
164
166
|
}
|
|
165
167
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
166
|
-
// Stamping (`fx-factory-tickets §6.2`)
|
|
167
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
168
|
-
/** Canonical header field the delivered URL is stamped into. It is the ONLY field a tool writes. */
|
|
169
|
-
const GITHUB_ISSUE_FIELD = '**GitHub issue:**';
|
|
170
|
-
const ESTADO_FIELD = '**Estado:**';
|
|
171
|
-
/**
|
|
172
|
-
* Write `url` into the ticket's `**GitHub issue:**` slot, which is born holding an
|
|
173
|
-
* em-dash and sits immediately after `**Estado:**`. Stamping the URL does NOT touch
|
|
174
|
-
* `Estado` — delivering is not triaging (`fx-factory-tickets §6.2`).
|
|
175
|
-
*
|
|
176
|
-
* A ticket written before the convention has no such field; it is inserted right
|
|
177
|
-
* after `Estado` rather than dropped, so the ticket stops being a dead end either
|
|
178
|
-
* way. With no header block at all, nothing is changed and the caller warns.
|
|
179
|
-
*/
|
|
180
|
-
export function stampGitHubIssueUrl(content, url) {
|
|
181
|
-
const stamped = `${GITHUB_ISSUE_FIELD} ${url}`;
|
|
182
|
-
const lines = content.split('\n');
|
|
183
|
-
const fieldIdx = lines.findIndex((line) => line.startsWith(GITHUB_ISSUE_FIELD));
|
|
184
|
-
if (fieldIdx >= 0) {
|
|
185
|
-
lines[fieldIdx] = stamped;
|
|
186
|
-
return { content: lines.join('\n'), changed: true };
|
|
187
|
-
}
|
|
188
|
-
const estadoIdx = lines.findIndex((line) => line.startsWith(ESTADO_FIELD));
|
|
189
|
-
if (estadoIdx >= 0) {
|
|
190
|
-
lines.splice(estadoIdx + 1, 0, stamped);
|
|
191
|
-
return { content: lines.join('\n'), changed: true };
|
|
192
|
-
}
|
|
193
|
-
return { content, changed: false };
|
|
194
|
-
}
|
|
195
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
196
168
|
// Terminal safety
|
|
197
169
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
198
170
|
/**
|
|
@@ -271,7 +243,7 @@ async function defaultConfirm(message) {
|
|
|
271
243
|
*
|
|
272
244
|
* Order matters and is asserted by the unit test: containment → read → size →
|
|
273
245
|
* (`--dry-run` stops here) → gh session → publish gate → create → verify
|
|
274
|
-
* assignee/label →
|
|
246
|
+
* assignee/label → delete the draft. Every guard that can reject runs BEFORE `gh` is ever
|
|
275
247
|
* spawned.
|
|
276
248
|
*/
|
|
277
249
|
export async function runTicketPush(filePath, flags = parseTicketPushFlags([]), rootDir = detectRepo(process.cwd()).repoRoot ?? process.cwd(), deps = {}) {
|
|
@@ -387,8 +359,8 @@ export async function runTicketPush(filePath, flags = parseTicketPushFlags([]),
|
|
|
387
359
|
if (!url) {
|
|
388
360
|
console.log(`✅ Issue creado en ${FACTORY_REPO}.`);
|
|
389
361
|
console.log(stdout.trim());
|
|
390
|
-
console.warn('⚠️ No se pudo leer la URL de la salida de `gh`, así que el
|
|
391
|
-
'
|
|
362
|
+
console.warn('⚠️ No se pudo leer la URL de la salida de `gh`, así que el borrador local NO se borró. ' +
|
|
363
|
+
'Revisa el issue de arriba y borra el archivo a mano (no lo vuelvas a entregar: abriría otro issue).');
|
|
392
364
|
return;
|
|
393
365
|
}
|
|
394
366
|
console.log(`✅ Issue creado: ${url}`);
|
|
@@ -407,22 +379,19 @@ export async function runTicketPush(filePath, flags = parseTicketPushFlags([]),
|
|
|
407
379
|
// permission is `read` — so the two notification mechanisms of this channel can
|
|
408
380
|
// fail at once, without a word, unless we look.
|
|
409
381
|
await verifyIssueFields(url, flags, labelDropped, assigneeDropped);
|
|
410
|
-
// 6.
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
console.warn(`⚠️ \`${shown}\` no tiene el campo \`${GITHUB_ISSUE_FIELD}\` ni \`${ESTADO_FIELD}\`, así que no se estampó. ` +
|
|
415
|
-
'Agrega la URL a mano (`fx-factory-tickets §6.2`).');
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
382
|
+
// 6. The draft is delivered: delete it. From here the issue is the only source of
|
|
383
|
+
// state. If this fails, the URL is already on screen (step above) — never leave
|
|
384
|
+
// the dev without the URL of an issue that already exists, and never fail a
|
|
385
|
+
// delivery that succeeded.
|
|
418
386
|
try {
|
|
419
|
-
(deps.
|
|
420
|
-
console.log(`
|
|
387
|
+
(deps.removeFile ?? unlinkSync)(target);
|
|
388
|
+
console.log(` Borré el borrador \`${shown}\`: el estado vive en el issue. ` +
|
|
389
|
+
'Commitea el borrado (`docs(factory): …`).');
|
|
421
390
|
}
|
|
422
391
|
catch (error) {
|
|
423
|
-
console.warn(`⚠️ El issue existe (${url}) pero \`${shown}\` no se pudo
|
|
392
|
+
console.warn(`⚠️ El issue existe (${url}) pero \`${shown}\` no se pudo borrar: ` +
|
|
424
393
|
`${error instanceof Error ? error.message : String(error)}\n` +
|
|
425
|
-
|
|
394
|
+
' Bórralo a mano; entregarlo otra vez abriría un issue duplicado.');
|
|
426
395
|
}
|
|
427
396
|
}
|
|
428
397
|
/**
|
package/dist/commands/update.js
CHANGED
|
@@ -52,7 +52,7 @@ import { printProvisionPointer } from '../lib/provision-pointer.js';
|
|
|
52
52
|
import { printRetrofitReminder } from '../lib/retrofit-reminder.js';
|
|
53
53
|
import { hasProvisionState } from '../lib/provision-state.js';
|
|
54
54
|
import { loginCommand } from '../lib/rail.js';
|
|
55
|
-
import {
|
|
55
|
+
import { resolveBrainRoot } from '../lib/repo-detection.js';
|
|
56
56
|
import { downloadBetaProfileTarball, downloadProfileTarball, stageProfileTarball, } from '../lib/unpack.js';
|
|
57
57
|
/** Parse `update`'s argv slice into flags. Unknown flags are ignored here. */
|
|
58
58
|
export function parseUpdateFlags(argv) {
|
|
@@ -795,8 +795,9 @@ export async function maybeOfferEmptyE2EBranch(rootDir, flags, opts = {}) {
|
|
|
795
795
|
export async function runUpdate(flags, deps = {}) {
|
|
796
796
|
// Operate on the REPO ROOT (the brain + lockfile live there), not cwd — running
|
|
797
797
|
// `update` from a subdir of a managed repo must not miss the lockfile and
|
|
798
|
-
// legacy-register a second nested brain (consistency with `add`).
|
|
799
|
-
|
|
798
|
+
// legacy-register a second nested brain (consistency with `add`). Outside a repo,
|
|
799
|
+
// or with a root that is `~` or an ancestor of it, it stops before anything else.
|
|
800
|
+
const rootDir = resolveBrainRoot(process.cwd());
|
|
800
801
|
// Mutually-exclusive flag validation FIRST — before --verify/--resume
|
|
801
802
|
// short-circuit, so an invalid combination is always rejected (not silently
|
|
802
803
|
// honored by verify/resume).
|
package/dist/commands/vault.js
CHANGED
|
@@ -34,6 +34,7 @@ import path from 'node:path';
|
|
|
34
34
|
import { execa } from 'execa';
|
|
35
35
|
import prompts from 'prompts';
|
|
36
36
|
import { CLIError } from '../lib/cli-error.js';
|
|
37
|
+
import { sendingDomainOf } from '../lib/email-from.js';
|
|
37
38
|
import { WITH_VAULT_SCRIPT_FILE } from '../lib/constants.js';
|
|
38
39
|
import { parseEnvBody } from '../lib/env-wizard.js';
|
|
39
40
|
import { hasLockfile, readLockfile } from '../lib/lockfile.js';
|
|
@@ -53,12 +54,14 @@ import { computeStatus } from './status.js';
|
|
|
53
54
|
// ---------------------------------------------------------------------------
|
|
54
55
|
// Help + flags
|
|
55
56
|
// ---------------------------------------------------------------------------
|
|
57
|
+
/** The vault (Infisical) refuses a project slug shorter than this. */
|
|
58
|
+
export const VAULT_SLUG_MIN_LENGTH = 5;
|
|
56
59
|
/** What `factory vault` prints without a subcommand. */
|
|
57
60
|
export const VAULT_HELP = `
|
|
58
61
|
factory vault — la bóveda de secretos de la organización (fx-secrets-vault).
|
|
59
62
|
|
|
60
63
|
Uso:
|
|
61
|
-
factory vault adopt [--apply] [--json] [--team=<id>] [--vercel-project <id>]
|
|
64
|
+
factory vault adopt [--apply] [--json] [--team=<id>] [--vercel-project <id>] [--slug <slug>]
|
|
62
65
|
factory vault adopt --decline
|
|
63
66
|
factory vault sync railway --project <id> --service <id> --env <main|develop> [--railway-env <nombre>]
|
|
64
67
|
factory vault deploys [--deploy <slug>] [--apply] [--yes]
|
|
@@ -83,6 +86,10 @@ Subcomandos:
|
|
|
83
86
|
Con varios proyectos de Vercel conectados al repo, elige el que queda como
|
|
84
87
|
proyecto principal (tiene que ser uno de ellos). Los demás despliegues se
|
|
85
88
|
operan después con \`factory vault deploys\`.
|
|
89
|
+
--slug <slug>
|
|
90
|
+
Slug del proyecto en la bóveda (mínimo ${VAULT_SLUG_MIN_LENGTH} caracteres). Default: el
|
|
91
|
+
que registra .timekast/provision.json, si no el proyecto de Vercel, si no
|
|
92
|
+
el repo de origin, y sólo al final el nombre del directorio.
|
|
86
93
|
--decline Registra en .timekast/provision.json que este repo NO entra a la bóveda:
|
|
87
94
|
sigue con .env.local y env:push, y doctor deja de proponerlo. No toca nada
|
|
88
95
|
más ni consulta ningún servicio. Adoptar después sigue siendo posible.
|
|
@@ -140,15 +147,32 @@ export function parseVaultAdoptFlags(argv) {
|
|
|
140
147
|
}
|
|
141
148
|
flags.vercelProject = value.trim();
|
|
142
149
|
}
|
|
150
|
+
else if (arg === '--slug' || arg.startsWith('--slug=')) {
|
|
151
|
+
let value;
|
|
152
|
+
if (arg.startsWith('--slug='))
|
|
153
|
+
value = arg.slice('--slug='.length);
|
|
154
|
+
else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith('--')) {
|
|
155
|
+
value = argv[i + 1];
|
|
156
|
+
i += 1;
|
|
157
|
+
}
|
|
158
|
+
if (value === undefined || value.trim() === '') {
|
|
159
|
+
throw new CLIError('`--slug` necesita el slug del proyecto en la bóveda.');
|
|
160
|
+
}
|
|
161
|
+
flags.slug = value.trim().toLowerCase();
|
|
162
|
+
}
|
|
143
163
|
else {
|
|
144
|
-
throw new CLIError(`Opción desconocida para \`factory vault adopt\`: \`${arg}\`. Uso: \`factory vault adopt [--apply] [--json] [--team=<id>] [--vercel-project <id>]\` o \`factory vault adopt --decline\`.`);
|
|
164
|
+
throw new CLIError(`Opción desconocida para \`factory vault adopt\`: \`${arg}\`. Uso: \`factory vault adopt [--apply] [--json] [--team=<id>] [--vercel-project <id>] [--slug <slug>]\` o \`factory vault adopt --decline\`.`);
|
|
145
165
|
}
|
|
146
166
|
}
|
|
147
167
|
if (flags.apply && flags.json) {
|
|
148
168
|
throw new CLIError('`--json` es sólo para el plan (sin `--apply`).');
|
|
149
169
|
}
|
|
150
170
|
if (flags.decline &&
|
|
151
|
-
(flags.apply ||
|
|
171
|
+
(flags.apply ||
|
|
172
|
+
flags.json ||
|
|
173
|
+
flags.team !== undefined ||
|
|
174
|
+
flags.vercelProject !== undefined ||
|
|
175
|
+
flags.slug !== undefined)) {
|
|
152
176
|
throw new CLIError('`--decline` va solo: registra que el repo no entra a la bóveda, sin plan ni `--apply`.');
|
|
153
177
|
}
|
|
154
178
|
return flags;
|
|
@@ -937,9 +961,19 @@ async function prepareAdoption(rootDir, flags, deps) {
|
|
|
937
961
|
slug = fromConfig;
|
|
938
962
|
}
|
|
939
963
|
else {
|
|
940
|
-
|
|
964
|
+
// The origin repo's name comes before the directory's: a worktree or a renamed checkout
|
|
965
|
+
// would otherwise name the vault project after the folder.
|
|
966
|
+
slug = (flags.slug ??
|
|
967
|
+
realField(target?.slug) ??
|
|
968
|
+
vercelProject?.name ??
|
|
969
|
+
repo?.split('/')[1] ??
|
|
970
|
+
path.basename(rootDir)).toLowerCase();
|
|
941
971
|
}
|
|
942
972
|
assertValidSubdomainSlug(slug);
|
|
973
|
+
if (slug.length < VAULT_SLUG_MIN_LENGTH) {
|
|
974
|
+
throw new CLIError(`El slug \`${slug}\` tiene menos de ${VAULT_SLUG_MIN_LENGTH} caracteres y la bóveda no acepta ` +
|
|
975
|
+
`crear un proyecto así. Elige uno con \`--slug <slug>\` (p. ej. \`${slug}-web\`). No escribí nada.`);
|
|
976
|
+
}
|
|
943
977
|
const railwayToken = await rail.getOptional(RAILWAY_TOKEN_KEY);
|
|
944
978
|
if (!railwayToken) {
|
|
945
979
|
console.warn('⚠ No hay `RAILWAY_TOKEN` en `rail-timekast`: no inventarié Railway. Si este repo despliega ahí, ' +
|
|
@@ -981,7 +1015,7 @@ async function prepareAdoption(rootDir, flags, deps) {
|
|
|
981
1015
|
values.preview.get('EMAIL_FROM') ??
|
|
982
1016
|
values.local.get('EMAIL_FROM') ??
|
|
983
1017
|
PROVISION_EMAIL_FROM;
|
|
984
|
-
const resendDomain =
|
|
1018
|
+
const resendDomain = sendingDomainOf(sender);
|
|
985
1019
|
const client = deps.vaultClient ?? createVaultClient();
|
|
986
1020
|
const githubSecretNames = repo
|
|
987
1021
|
? [...(await (deps.listGithubSecretNames ?? ghSecretNames)(repo))].sort()
|
package/dist/index.js
CHANGED
|
@@ -191,8 +191,8 @@ Sobre \`backlog\`:
|
|
|
191
191
|
|
|
192
192
|
Sobre \`ticket\`:
|
|
193
193
|
\`ticket push <archivo>\` entrega un factory-ticket de \`project/factory/\` al repo
|
|
194
|
-
del Factory como un issue de GitHub
|
|
195
|
-
|
|
194
|
+
del Factory como un issue de GitHub y, entregado, BORRA el archivo local: era un
|
|
195
|
+
borrador, y el estado vive en el issue (commitea el borrado con \`docs(factory): …\`).
|
|
196
196
|
Cero credenciales nuevas: usa tu propia sesión de \`gh\`; sin sesión avisa y sale 0.
|
|
197
197
|
El cuerpo se publica ÍNTEGRO en un repo compartido, así que hay compuerta:
|
|
198
198
|
--dry-run Imprime destino, asignado, etiqueta y título; no crea nada
|
package/dist/lib/claude-md.js
CHANGED
|
@@ -224,8 +224,10 @@ export function classifyClaudeMd(opts) {
|
|
|
224
224
|
return { case: 'A', action: 'none', uncovered: [] };
|
|
225
225
|
}
|
|
226
226
|
const uncovered = uncoveredKitRules(content, kitRules);
|
|
227
|
-
|
|
228
|
-
|
|
227
|
+
// `skip-core` on case C only shapes the advice: `core` has no INDEX to point at.
|
|
228
|
+
if (uncovered.length > 0) {
|
|
229
|
+
return { case: 'C', action: profile === 'core' ? 'skip-core' : 'none', uncovered };
|
|
230
|
+
}
|
|
229
231
|
if (hasIndexImport(content))
|
|
230
232
|
return { case: 'B', action: 'already-present', uncovered: [] };
|
|
231
233
|
// The `core` profile does not ship `.claude/rules/INDEX.md` at all (it resolves its
|
|
@@ -296,7 +298,10 @@ export function reportClaudeMd(assessment, mode, inserted = true) {
|
|
|
296
298
|
if (kind === 'C') {
|
|
297
299
|
console.log(`\n\`${CLAUDE_MD_FILE}\` (caso C — necesita revisión manual): lo editaste y le faltan rules del kit ` +
|
|
298
300
|
`en los imports: ${uncovered.join(', ')}.`);
|
|
299
|
-
console.log(
|
|
301
|
+
console.log(action === 'skip-core'
|
|
302
|
+
? ` No se tocó nada. Repón a mano los imports que falten (\`@<ruta>\` por cada una); ` +
|
|
303
|
+
`el perfil \`core\` no incluye \`${RULES_INDEX_PATH}\`.`
|
|
304
|
+
: ` No se tocó nada. Agrega \`${RULES_INDEX_IMPORT}\` a mano (importa todas) o repón los imports que falten.`);
|
|
300
305
|
return;
|
|
301
306
|
}
|
|
302
307
|
// Case B — edited outside the kit's zone.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sending domain of an `EMAIL_FROM` value — the domain a project's Resend key is limited to.
|
|
3
|
+
*
|
|
4
|
+
* `EMAIL_FROM` comes in two shapes, and a live deploy carries either: the bare address the kit
|
|
5
|
+
* writes (`noreply@updates.timekast.com`) and the RFC 5322 display-name form a team sets so the
|
|
6
|
+
* inbox shows a name (`TimeKast Calendar <noreply@tk-cal.com>`). Taking everything after the `@`
|
|
7
|
+
* of the raw value turns the second one into `tk-cal.com>` — a domain Resend never lists, so
|
|
8
|
+
* `vault adopt --apply` aborted on a perfectly valid sender.
|
|
9
|
+
*
|
|
10
|
+
* Rule: the address is the part inside the LAST `<…>` when there is one, else the whole value;
|
|
11
|
+
* the domain is what follows its LAST `@`, trimmed and lower-cased (DNS names are
|
|
12
|
+
* case-insensitive and Resend lists them lower-case). No `@` at all → `''`, which matches no
|
|
13
|
+
* Resend domain, so the caller's "domain not registered" path stops the run instead of minting
|
|
14
|
+
* a key for a guessed domain.
|
|
15
|
+
*
|
|
16
|
+
* 🔒 TOTAL (never throws): it runs on values read from a deploy, which the CLI does not control.
|
|
17
|
+
*/
|
|
18
|
+
export function sendingDomainOf(emailFrom) {
|
|
19
|
+
const value = emailFrom.trim();
|
|
20
|
+
const open = value.lastIndexOf('<');
|
|
21
|
+
const close = value.lastIndexOf('>');
|
|
22
|
+
const address = open !== -1 && close > open ? value.slice(open + 1, close) : value;
|
|
23
|
+
const at = address.lastIndexOf('@');
|
|
24
|
+
return at === -1
|
|
25
|
+
? ''
|
|
26
|
+
: address
|
|
27
|
+
.slice(at + 1)
|
|
28
|
+
.trim()
|
|
29
|
+
.toLowerCase();
|
|
30
|
+
}
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* is a valid context (DIST-004 §8). `add` installs in the CWD, not the repo
|
|
10
10
|
* root — `repoRoot` is informational (e.g. monorepo with `.git/` in an ancestor).
|
|
11
11
|
*/
|
|
12
|
-
import { existsSync } from 'node:fs';
|
|
12
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
13
14
|
import path from 'node:path';
|
|
15
|
+
import { CLIError } from './cli-error.js';
|
|
14
16
|
/**
|
|
15
17
|
* Detect whether `cwd` (or any ancestor) is inside a git repository.
|
|
16
18
|
*
|
|
@@ -30,3 +32,42 @@ export function detectRepo(cwd) {
|
|
|
30
32
|
}
|
|
31
33
|
return { hasRepo: false, repoRoot: null };
|
|
32
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* The repo root a brain-writing command (`update`, `add`) may install into, or a
|
|
37
|
+
* `CLIError` before anything is read, downloaded or written.
|
|
38
|
+
*
|
|
39
|
+
* Refuses outside a git repo (no silent fallback to `cwd`) and refuses a root that
|
|
40
|
+
* is the home directory or one of its ancestors: run from `~`, or from a `~` that
|
|
41
|
+
* versions dotfiles in a `~/.git`, the manifest would land on `~/.claude`. No flag
|
|
42
|
+
* skips it — `--yes` only answers prompts, it never widens the target.
|
|
43
|
+
*
|
|
44
|
+
* @param cwd Directory the command was run from.
|
|
45
|
+
* @param home The user's home directory (injectable for tests).
|
|
46
|
+
*/
|
|
47
|
+
export function resolveBrainRoot(cwd, home = homedir()) {
|
|
48
|
+
const { repoRoot } = detectRepo(cwd);
|
|
49
|
+
if (!repoRoot) {
|
|
50
|
+
throw new CLIError('No hay repositorio git aquí. Corre el comando desde la raíz de tu proyecto ' +
|
|
51
|
+
'(o `git init` primero, o `factory new` para crear uno desde cero). No toqué nada.');
|
|
52
|
+
}
|
|
53
|
+
// Compared through symlinks: `cwd` arrives resolved (`/private/var/...` on macOS)
|
|
54
|
+
// while `HOME` may name the same folder by its link.
|
|
55
|
+
const root = path.resolve(repoRoot);
|
|
56
|
+
// Home is at or under the root unless the way from root to home climbs out first.
|
|
57
|
+
const relative = path.relative(realOrResolved(root), realOrResolved(home));
|
|
58
|
+
const homeOutsideRoot = relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
|
|
59
|
+
if (!homeOutsideRoot) {
|
|
60
|
+
throw new CLIError(`La raíz del repo es \`${root}\`, que es tu carpeta personal o la contiene. ` +
|
|
61
|
+
'El kit solo se instala dentro de un proyecto: corre el comando desde la carpeta del proyecto. No toqué nada.');
|
|
62
|
+
}
|
|
63
|
+
return root;
|
|
64
|
+
}
|
|
65
|
+
/** `realpath` of `p`, or its plain resolution when it does not exist. */
|
|
66
|
+
function realOrResolved(p) {
|
|
67
|
+
try {
|
|
68
|
+
return realpathSync(p);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return path.resolve(p);
|
|
72
|
+
}
|
|
73
|
+
}
|