@slowcook-ai/cli 0.16.0-alpha.4 → 0.17.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +50 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/brew/agent.d.ts +25 -1
- package/dist/commands/brew/agent.d.ts.map +1 -1
- package/dist/commands/brew/agent.js +123 -20
- package/dist/commands/brew/agent.js.map +1 -1
- package/dist/commands/brew/halt.d.ts +1 -1
- package/dist/commands/brew/halt.d.ts.map +1 -1
- package/dist/commands/brew/halt.js +13 -0
- package/dist/commands/brew/halt.js.map +1 -1
- package/dist/commands/check/index.d.ts +14 -0
- package/dist/commands/check/index.d.ts.map +1 -0
- package/dist/commands/check/index.js +75 -0
- package/dist/commands/check/index.js.map +1 -0
- package/dist/commands/check/mock-isolation.d.ts +52 -0
- package/dist/commands/check/mock-isolation.d.ts.map +1 -0
- package/dist/commands/check/mock-isolation.js +186 -0
- package/dist/commands/check/mock-isolation.js.map +1 -0
- package/dist/commands/init/from-prod.d.ts +48 -0
- package/dist/commands/init/from-prod.d.ts.map +1 -0
- package/dist/commands/init/from-prod.js +256 -0
- package/dist/commands/init/from-prod.js.map +1 -0
- package/dist/commands/init/index.d.ts.map +1 -1
- package/dist/commands/init/index.js +9 -0
- package/dist/commands/init/index.js.map +1 -1
- package/dist/commands/init/mock.d.ts.map +1 -1
- package/dist/commands/init/mock.js +47 -13
- package/dist/commands/init/mock.js.map +1 -1
- package/dist/commands/on-mockup-approved/index.d.ts +25 -0
- package/dist/commands/on-mockup-approved/index.d.ts.map +1 -0
- package/dist/commands/on-mockup-approved/index.js +359 -0
- package/dist/commands/on-mockup-approved/index.js.map +1 -0
- package/dist/commands/plate/classify.d.ts +65 -0
- package/dist/commands/plate/classify.d.ts.map +1 -0
- package/dist/commands/plate/classify.js +194 -0
- package/dist/commands/plate/classify.js.map +1 -0
- package/dist/commands/plate/index.d.ts.map +1 -1
- package/dist/commands/plate/index.js +259 -34
- package/dist/commands/plate/index.js.map +1 -1
- package/dist/commands/port/index.d.ts +30 -0
- package/dist/commands/port/index.d.ts.map +1 -0
- package/dist/commands/port/index.js +237 -0
- package/dist/commands/port/index.js.map +1 -0
- package/dist/commands/port/transform.d.ts +68 -0
- package/dist/commands/port/transform.d.ts.map +1 -0
- package/dist/commands/port/transform.js +122 -0
- package/dist/commands/port/transform.js.map +1 -0
- package/dist/commands/preview/config.d.ts +73 -0
- package/dist/commands/preview/config.d.ts.map +1 -0
- package/dist/commands/preview/config.js +200 -0
- package/dist/commands/preview/config.js.map +1 -0
- package/dist/commands/preview/deploy.d.ts +35 -0
- package/dist/commands/preview/deploy.d.ts.map +1 -0
- package/dist/commands/preview/deploy.js +247 -0
- package/dist/commands/preview/deploy.js.map +1 -0
- package/dist/commands/preview/index.d.ts +9 -0
- package/dist/commands/preview/index.d.ts.map +1 -0
- package/dist/commands/preview/index.js +67 -0
- package/dist/commands/preview/index.js.map +1 -0
- package/dist/commands/preview/ssh.d.ts +49 -0
- package/dist/commands/preview/ssh.d.ts.map +1 -0
- package/dist/commands/preview/ssh.js +99 -0
- package/dist/commands/preview/ssh.js.map +1 -0
- package/dist/commands/preview/teardown.d.ts +25 -0
- package/dist/commands/preview/teardown.d.ts.map +1 -0
- package/dist/commands/preview/teardown.js +164 -0
- package/dist/commands/preview/teardown.js.map +1 -0
- package/dist/commands/recon/index.d.ts +60 -0
- package/dist/commands/recon/index.d.ts.map +1 -0
- package/dist/commands/recon/index.js +278 -0
- package/dist/commands/recon/index.js.map +1 -0
- package/dist/commands/refine/context.d.ts +12 -0
- package/dist/commands/refine/context.d.ts.map +1 -1
- package/dist/commands/refine/context.js +72 -0
- package/dist/commands/refine/context.js.map +1 -1
- package/dist/commands/refine/history-index.d.ts +84 -0
- package/dist/commands/refine/history-index.d.ts.map +1 -0
- package/dist/commands/refine/history-index.js +289 -0
- package/dist/commands/refine/history-index.js.map +1 -0
- package/dist/commands/refine/index.d.ts.map +1 -1
- package/dist/commands/refine/index.js +28 -0
- package/dist/commands/refine/index.js.map +1 -1
- package/dist/commands/run-mock/index.d.ts +34 -0
- package/dist/commands/run-mock/index.d.ts.map +1 -0
- package/dist/commands/run-mock/index.js +308 -0
- package/dist/commands/run-mock/index.js.map +1 -0
- package/dist/commands/vibe/index.d.ts.map +1 -1
- package/dist/commands/vibe/index.js +38 -4
- package/dist/commands/vibe/index.js.map +1 -1
- package/package.json +15 -13
- package/LICENSE +0 -21
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin shell wrappers around `ssh` + `scp` so deploy/teardown can talk
|
|
3
|
+
* to the consumer's box without pulling in a JS ssh library.
|
|
4
|
+
*
|
|
5
|
+
* Why shell out: ops people debug `ssh` + `scp` invocations daily; if a
|
|
6
|
+
* deploy fails, the failing command is something they can copy + paste.
|
|
7
|
+
* Pulling in a JS library for marginal API ergonomics adds a maintenance
|
|
8
|
+
* surface for a feature that's already box-specific.
|
|
9
|
+
*
|
|
10
|
+
* All wrappers use BatchMode=yes (no interactive password prompts) and
|
|
11
|
+
* StrictHostKeyChecking=accept-new (first connect adds the key, future
|
|
12
|
+
* connects verify against it). Failures throw with stderr captured so
|
|
13
|
+
* the workflow logs surface the underlying error.
|
|
14
|
+
*/
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
/**
|
|
17
|
+
* Standard ssh options: non-interactive, accept-new host keys, dedicated
|
|
18
|
+
* key file. Returns the array form spawnSync wants.
|
|
19
|
+
*/
|
|
20
|
+
function sshArgs(target, extra) {
|
|
21
|
+
return [
|
|
22
|
+
"-i", target.keyPath,
|
|
23
|
+
"-p", String(target.port),
|
|
24
|
+
"-o", "BatchMode=yes",
|
|
25
|
+
"-o", "StrictHostKeyChecking=accept-new",
|
|
26
|
+
"-o", "ConnectTimeout=15",
|
|
27
|
+
`${target.user}@${target.host}`,
|
|
28
|
+
...extra,
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Run a remote command via ssh. Throws on non-zero exit with the
|
|
33
|
+
* captured stderr inlined into the error message.
|
|
34
|
+
*/
|
|
35
|
+
export function sshExec(target, command) {
|
|
36
|
+
const result = spawnSync("ssh", sshArgs(target, [command]), {
|
|
37
|
+
encoding: "utf8",
|
|
38
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
39
|
+
});
|
|
40
|
+
if (result.error) {
|
|
41
|
+
throw new Error(`ssh spawn failed: ${result.error.message}`);
|
|
42
|
+
}
|
|
43
|
+
if (result.status !== 0) {
|
|
44
|
+
throw new Error(`ssh exited ${result.status} for command:\n ${command}\nstderr:\n${(result.stderr || "").trim()}`);
|
|
45
|
+
}
|
|
46
|
+
return { stdout: result.stdout || "", stderr: result.stderr || "" };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* scp a local file to a remote path. Throws on non-zero exit.
|
|
50
|
+
*/
|
|
51
|
+
export function scpUpload(target, localPath, remotePath) {
|
|
52
|
+
const result = spawnSync("scp", [
|
|
53
|
+
"-i", target.keyPath,
|
|
54
|
+
"-P", String(target.port),
|
|
55
|
+
"-o", "BatchMode=yes",
|
|
56
|
+
"-o", "StrictHostKeyChecking=accept-new",
|
|
57
|
+
"-o", "ConnectTimeout=15",
|
|
58
|
+
localPath,
|
|
59
|
+
`${target.user}@${target.host}:${remotePath}`,
|
|
60
|
+
], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
61
|
+
if (result.error) {
|
|
62
|
+
throw new Error(`scp spawn failed: ${result.error.message}`);
|
|
63
|
+
}
|
|
64
|
+
if (result.status !== 0) {
|
|
65
|
+
throw new Error(`scp exited ${result.status} uploading ${localPath} → ${target.user}@${target.host}:${remotePath}\nstderr:\n${(result.stderr || "").trim()}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Find the first free TCP port in [lo, hi] on the remote box. Uses
|
|
70
|
+
* `ss -ltn` (universally available on modern Linux) to enumerate
|
|
71
|
+
* already-bound ports.
|
|
72
|
+
*
|
|
73
|
+
* Returns the chosen port. Throws if every port in the range is in use.
|
|
74
|
+
*/
|
|
75
|
+
export function pickRemotePort(target, lo, hi) {
|
|
76
|
+
const cmd = `ss -ltn 'sport = :0' 2>/dev/null | awk 'NR>1 {split($4,a,":"); print a[length(a)]}' | sort -un`;
|
|
77
|
+
const { stdout } = sshExec(target, cmd);
|
|
78
|
+
const used = new Set(stdout
|
|
79
|
+
.split("\n")
|
|
80
|
+
.map((s) => parseInt(s.trim(), 10))
|
|
81
|
+
.filter((n) => !Number.isNaN(n)));
|
|
82
|
+
for (let p = lo; p <= hi; p++) {
|
|
83
|
+
if (!used.has(p))
|
|
84
|
+
return p;
|
|
85
|
+
}
|
|
86
|
+
throw new Error(`No free port in range ${lo}..${hi} on ${target.host}. Increase port_range or teardown stale containers.`);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get the host port a running container is publishing to (the value
|
|
90
|
+
* after `0.0.0.0:` in `docker port` output). Returns null if the
|
|
91
|
+
* container isn't running or doesn't publish 3100.
|
|
92
|
+
*/
|
|
93
|
+
export function getContainerPort(target, containerName) {
|
|
94
|
+
const cmd = `docker port ${containerName} 3100/tcp 2>/dev/null || true`;
|
|
95
|
+
const { stdout } = sshExec(target, cmd);
|
|
96
|
+
const m = stdout.trim().match(/:(\d+)\s*$/m);
|
|
97
|
+
return m ? parseInt(m[1], 10) : null;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=ssh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssh.js","sourceRoot":"","sources":["../../../src/commands/preview/ssh.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `slowcook preview teardown --pr <n>` — 0.16.0-α.5.
|
|
3
|
+
*
|
|
4
|
+
* Removes the running container + cleans up the staging directory for
|
|
5
|
+
* a given PR. Idempotent — running it twice is fine. Updates the PR
|
|
6
|
+
* comment to reflect the teardown.
|
|
7
|
+
*
|
|
8
|
+
* Triggered by the slowcook-preview-teardown.yml workflow on
|
|
9
|
+
* `pull_request: closed`. Safe to also run manually via
|
|
10
|
+
* workflow_dispatch when a stale container needs purging.
|
|
11
|
+
*/
|
|
12
|
+
interface ParsedArgs {
|
|
13
|
+
pr: number | undefined;
|
|
14
|
+
repoRoot: string;
|
|
15
|
+
keyPath: string | undefined;
|
|
16
|
+
owner: string | undefined;
|
|
17
|
+
repo: string | undefined;
|
|
18
|
+
/** Also remove the docker image (frees disk on the box). Default: keep image for fast redeploy. */
|
|
19
|
+
pruneImage: boolean;
|
|
20
|
+
dryRun: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function parseTeardownArgs(argv: string[]): ParsedArgs;
|
|
23
|
+
export declare function teardown(argv: string[], cliVersion: string): Promise<void>;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=teardown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teardown.d.ts","sourceRoot":"","sources":["../../../src/commands/preview/teardown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAWH,UAAU,UAAU;IAClB,EAAE,EAAE,MAAM,GAAG,SAAS,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,mGAAmG;IACnG,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAsB5D;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA+DhF"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `slowcook preview teardown --pr <n>` — 0.16.0-α.5.
|
|
3
|
+
*
|
|
4
|
+
* Removes the running container + cleans up the staging directory for
|
|
5
|
+
* a given PR. Idempotent — running it twice is fine. Updates the PR
|
|
6
|
+
* comment to reflect the teardown.
|
|
7
|
+
*
|
|
8
|
+
* Triggered by the slowcook-preview-teardown.yml workflow on
|
|
9
|
+
* `pull_request: closed`. Safe to also run manually via
|
|
10
|
+
* workflow_dispatch when a stale container needs purging.
|
|
11
|
+
*/
|
|
12
|
+
import { execSync } from "node:child_process";
|
|
13
|
+
import { readPreviewConfig, containerNameForPr, imageTagForPr, remoteDirForPr, } from "./config.js";
|
|
14
|
+
import { sshExec } from "./ssh.js";
|
|
15
|
+
export function parseTeardownArgs(argv) {
|
|
16
|
+
const args = {
|
|
17
|
+
pr: undefined,
|
|
18
|
+
repoRoot: process.cwd(),
|
|
19
|
+
keyPath: undefined,
|
|
20
|
+
owner: undefined,
|
|
21
|
+
repo: undefined,
|
|
22
|
+
pruneImage: false,
|
|
23
|
+
dryRun: false,
|
|
24
|
+
};
|
|
25
|
+
for (let i = 0; i < argv.length; i++) {
|
|
26
|
+
const a = argv[i];
|
|
27
|
+
const next = argv[i + 1];
|
|
28
|
+
if (a === "--pr" && next) {
|
|
29
|
+
args.pr = parseInt(next, 10);
|
|
30
|
+
i++;
|
|
31
|
+
}
|
|
32
|
+
else if (a === "--cwd" && next) {
|
|
33
|
+
args.repoRoot = next;
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
else if (a === "--ssh-key" && next) {
|
|
37
|
+
args.keyPath = next;
|
|
38
|
+
i++;
|
|
39
|
+
}
|
|
40
|
+
else if (a === "--owner" && next) {
|
|
41
|
+
args.owner = next;
|
|
42
|
+
i++;
|
|
43
|
+
}
|
|
44
|
+
else if (a === "--repo" && next) {
|
|
45
|
+
args.repo = next;
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
else if (a === "--prune-image") {
|
|
49
|
+
args.pruneImage = true;
|
|
50
|
+
}
|
|
51
|
+
else if (a === "--dry-run") {
|
|
52
|
+
args.dryRun = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return args;
|
|
56
|
+
}
|
|
57
|
+
export async function teardown(argv, cliVersion) {
|
|
58
|
+
const parsed = parseTeardownArgs(argv);
|
|
59
|
+
if (!parsed.pr || Number.isNaN(parsed.pr)) {
|
|
60
|
+
console.error("--pr <number> is required.");
|
|
61
|
+
process.exit(64);
|
|
62
|
+
}
|
|
63
|
+
const cfg = readPreviewConfig(parsed.repoRoot);
|
|
64
|
+
const containerName = containerNameForPr(parsed.pr);
|
|
65
|
+
const imageTag = imageTagForPr(parsed.pr);
|
|
66
|
+
const remoteDir = remoteDirForPr(cfg, parsed.pr);
|
|
67
|
+
if (parsed.dryRun) {
|
|
68
|
+
console.log(`slowcook preview teardown · pr ${parsed.pr} (dry-run)`);
|
|
69
|
+
console.log(` would: docker rm -f ${containerName}`);
|
|
70
|
+
console.log(` would: rm -rf ${remoteDir}`);
|
|
71
|
+
if (parsed.pruneImage) {
|
|
72
|
+
console.log(` would: docker rmi ${imageTag}`);
|
|
73
|
+
}
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const keyPath = parsed.keyPath ?? process.env["SLOWCOOK_PREVIEW_SSH_KEY_PATH"];
|
|
77
|
+
if (!keyPath) {
|
|
78
|
+
console.error("SSH key path not provided. Pass --ssh-key <path> or set SLOWCOOK_PREVIEW_SSH_KEY_PATH.");
|
|
79
|
+
process.exit(2);
|
|
80
|
+
}
|
|
81
|
+
const target = {
|
|
82
|
+
host: cfg.host,
|
|
83
|
+
user: cfg.user,
|
|
84
|
+
port: cfg.port,
|
|
85
|
+
keyPath,
|
|
86
|
+
};
|
|
87
|
+
console.log(`slowcook preview teardown · pr ${parsed.pr} → ${cfg.user}@${cfg.host}`);
|
|
88
|
+
console.log(` ssh docker rm -f ${containerName}`);
|
|
89
|
+
sshExec(target, `docker rm -f ${shellQuote(containerName)} 2>/dev/null || true`);
|
|
90
|
+
if (parsed.pruneImage) {
|
|
91
|
+
console.log(` ssh docker rmi ${imageTag}`);
|
|
92
|
+
sshExec(target, `docker rmi ${shellQuote(imageTag)} 2>/dev/null || true`);
|
|
93
|
+
}
|
|
94
|
+
console.log(` ssh rm -rf ${remoteDir}`);
|
|
95
|
+
sshExec(target, `rm -rf ${shellQuote(remoteDir)}`);
|
|
96
|
+
// Update the PR comment to indicate teardown.
|
|
97
|
+
const githubToken = process.env["GITHUB_TOKEN"];
|
|
98
|
+
const owner = parsed.owner ?? detectOwner(parsed.repoRoot);
|
|
99
|
+
const repo = parsed.repo ?? detectRepo(parsed.repoRoot);
|
|
100
|
+
if (githubToken && owner && repo) {
|
|
101
|
+
await markCommentTornDown({
|
|
102
|
+
owner, repo, pr: parsed.pr, githubToken, cliVersion,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.log(` (skipped PR comment: no GITHUB_TOKEN or unknown owner/repo)`);
|
|
107
|
+
}
|
|
108
|
+
console.log(`Done. Preview for PR #${parsed.pr} is offline.`);
|
|
109
|
+
}
|
|
110
|
+
const PREVIEW_COMMENT_MARKER = "<!-- slowcook-preview-deploy -->";
|
|
111
|
+
async function markCommentTornDown(args) {
|
|
112
|
+
const { Octokit } = await import("@octokit/rest");
|
|
113
|
+
const octokit = new Octokit({ auth: args.githubToken });
|
|
114
|
+
const list = await octokit.rest.issues.listComments({
|
|
115
|
+
owner: args.owner,
|
|
116
|
+
repo: args.repo,
|
|
117
|
+
issue_number: args.pr,
|
|
118
|
+
per_page: 100,
|
|
119
|
+
});
|
|
120
|
+
const existing = list.data.find((c) => c.body?.includes(PREVIEW_COMMENT_MARKER));
|
|
121
|
+
if (!existing)
|
|
122
|
+
return;
|
|
123
|
+
const body = [
|
|
124
|
+
PREVIEW_COMMENT_MARKER,
|
|
125
|
+
`## 🍳 Mockup preview torn down`,
|
|
126
|
+
``,
|
|
127
|
+
`The preview container for PR #${args.pr} has been removed (PR closed).`,
|
|
128
|
+
``,
|
|
129
|
+
`_Torn down by \`slowcook preview teardown@${args.cliVersion}\`._`,
|
|
130
|
+
].join("\n");
|
|
131
|
+
await octokit.rest.issues.updateComment({
|
|
132
|
+
owner: args.owner,
|
|
133
|
+
repo: args.repo,
|
|
134
|
+
comment_id: existing.id,
|
|
135
|
+
body,
|
|
136
|
+
});
|
|
137
|
+
console.log(` pr comment ${existing.id} marked torn-down`);
|
|
138
|
+
}
|
|
139
|
+
function detectOwner(repoRoot) {
|
|
140
|
+
return detectOwnerRepo(repoRoot)?.owner;
|
|
141
|
+
}
|
|
142
|
+
function detectRepo(repoRoot) {
|
|
143
|
+
return detectOwnerRepo(repoRoot)?.repo;
|
|
144
|
+
}
|
|
145
|
+
function detectOwnerRepo(repoRoot) {
|
|
146
|
+
try {
|
|
147
|
+
const url = execSync("git remote get-url origin", {
|
|
148
|
+
cwd: repoRoot,
|
|
149
|
+
encoding: "utf8",
|
|
150
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
151
|
+
}).trim();
|
|
152
|
+
const m = url.match(/github\.com[:/]([^/]+)\/([^/.]+)(?:\.git)?$/);
|
|
153
|
+
if (m && m[1] && m[2])
|
|
154
|
+
return { owner: m[1], repo: m[2] };
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// not a git repo
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
function shellQuote(s) {
|
|
162
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=teardown.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teardown.js","sourceRoot":"","sources":["../../../src/commands/preview/teardown.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `slowcook recon` — 0.17.6 — structural backstop after refine + history-aware
|
|
3
|
+
* vibe + testgen + plate + brew. Pure deterministic; no LLM.
|
|
4
|
+
*
|
|
5
|
+
* Runs as a CI pre-step in slowcook-brew-auto.yml (after both mockup PR
|
|
6
|
+
* + tests PR are merged, BEFORE brew dispatch). Catches residual vibe ⇄
|
|
7
|
+
* testgen divergence by comparing names + prop shapes + testid hooks.
|
|
8
|
+
*
|
|
9
|
+
* Output: `.brewing/recon-result.json` + a PR comment with the renaming
|
|
10
|
+
* map and any escalations.
|
|
11
|
+
*
|
|
12
|
+
* Exit codes:
|
|
13
|
+
* 0 — clean (or only "warn" issues; brew can proceed)
|
|
14
|
+
* 2 — `STORY_HISTORY_CONFLICT` or `VIBE_RECIPE_NAME_DRIFT` — escalates
|
|
15
|
+
* to PM via PR comment; brew should NOT dispatch
|
|
16
|
+
*
|
|
17
|
+
* What recon checks:
|
|
18
|
+
* 1. Test imports → file exists in mock + src/
|
|
19
|
+
* 2. Test prop usage → matches mock's component signature
|
|
20
|
+
* 3. Testid selectors in tests → present in mock JSX
|
|
21
|
+
* 4. Brownfield safety: rename target collides with EXISTING prod
|
|
22
|
+
* component covered by ANOTHER story's tests
|
|
23
|
+
*/
|
|
24
|
+
import { type HistoryIndex } from "../refine/history-index.js";
|
|
25
|
+
interface RenameProposal {
|
|
26
|
+
kind: "component" | "import_path";
|
|
27
|
+
from: string;
|
|
28
|
+
to: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
rename_safe: boolean;
|
|
31
|
+
blocker?: string;
|
|
32
|
+
}
|
|
33
|
+
interface TestidGap {
|
|
34
|
+
selector: string;
|
|
35
|
+
queried_by: string[];
|
|
36
|
+
in_mock: boolean;
|
|
37
|
+
}
|
|
38
|
+
interface StructuralGap {
|
|
39
|
+
kind: "missing_component" | "missing_route" | "prop_shape_mismatch" | "story_history_conflict";
|
|
40
|
+
test: string;
|
|
41
|
+
detail: string;
|
|
42
|
+
recommendation: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ReconResult {
|
|
45
|
+
story: string;
|
|
46
|
+
generated_at: string;
|
|
47
|
+
generator: "slowcook-recon@0.17.6";
|
|
48
|
+
status: "clean" | "rename_needed" | "escalate";
|
|
49
|
+
renames: RenameProposal[];
|
|
50
|
+
testid_gaps: TestidGap[];
|
|
51
|
+
structural_gaps: StructuralGap[];
|
|
52
|
+
history_index_components: number;
|
|
53
|
+
warnings: string[];
|
|
54
|
+
}
|
|
55
|
+
export declare function recon(argv: string[], _cliVersion: string): Promise<void>;
|
|
56
|
+
export declare function findStoryTestFiles(repoRoot: string, story: string): string[];
|
|
57
|
+
export declare function extractImports(body: string): string[];
|
|
58
|
+
export declare function extractTestids(body: string): string[];
|
|
59
|
+
export type { HistoryIndex };
|
|
60
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/recon/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAalF,UAAU,cAAc;IACtB,IAAI,EAAE,WAAW,GAAG,aAAa,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,UAAU,aAAa;IACrB,IAAI,EAAE,mBAAmB,GAAG,eAAe,GAAG,qBAAqB,GAAG,wBAAwB,CAAC;IAC/F,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,uBAAuB,CAAC;IACnC,MAAM,EAAE,OAAO,GAAG,eAAe,GAAG,UAAU,CAAC;IAC/C,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,WAAW,EAAE,SAAS,EAAE,CAAC;IACzB,eAAe,EAAE,aAAa,EAAE,CAAC;IACjC,wBAAwB,EAAE,MAAM,CAAC;IACjC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAqDD,wBAAsB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkH9E;AAID,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAU5E;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAMrD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAYrD;AA8CD,YAAY,EAAE,YAAY,EAAE,CAAC"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `slowcook recon` — 0.17.6 — structural backstop after refine + history-aware
|
|
3
|
+
* vibe + testgen + plate + brew. Pure deterministic; no LLM.
|
|
4
|
+
*
|
|
5
|
+
* Runs as a CI pre-step in slowcook-brew-auto.yml (after both mockup PR
|
|
6
|
+
* + tests PR are merged, BEFORE brew dispatch). Catches residual vibe ⇄
|
|
7
|
+
* testgen divergence by comparing names + prop shapes + testid hooks.
|
|
8
|
+
*
|
|
9
|
+
* Output: `.brewing/recon-result.json` + a PR comment with the renaming
|
|
10
|
+
* map and any escalations.
|
|
11
|
+
*
|
|
12
|
+
* Exit codes:
|
|
13
|
+
* 0 — clean (or only "warn" issues; brew can proceed)
|
|
14
|
+
* 2 — `STORY_HISTORY_CONFLICT` or `VIBE_RECIPE_NAME_DRIFT` — escalates
|
|
15
|
+
* to PM via PR comment; brew should NOT dispatch
|
|
16
|
+
*
|
|
17
|
+
* What recon checks:
|
|
18
|
+
* 1. Test imports → file exists in mock + src/
|
|
19
|
+
* 2. Test prop usage → matches mock's component signature
|
|
20
|
+
* 3. Testid selectors in tests → present in mock JSX
|
|
21
|
+
* 4. Brownfield safety: rename target collides with EXISTING prod
|
|
22
|
+
* component covered by ANOTHER story's tests
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { dirname, join, relative } from "node:path";
|
|
26
|
+
import { buildHistoryIndex } from "../refine/history-index.js";
|
|
27
|
+
function parseArgs(argv) {
|
|
28
|
+
const args = {
|
|
29
|
+
story: "",
|
|
30
|
+
repoRoot: process.cwd(),
|
|
31
|
+
outPath: "",
|
|
32
|
+
verbose: false,
|
|
33
|
+
};
|
|
34
|
+
for (let i = 0; i < argv.length; i++) {
|
|
35
|
+
const a = argv[i];
|
|
36
|
+
const next = argv[i + 1];
|
|
37
|
+
if (a === "--story" && next) {
|
|
38
|
+
args.story = next;
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
else if (a === "--cwd" && next) {
|
|
42
|
+
args.repoRoot = next;
|
|
43
|
+
i++;
|
|
44
|
+
}
|
|
45
|
+
else if (a === "--out" && next) {
|
|
46
|
+
args.outPath = next;
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
else if (a === "--verbose" || a === "-v") {
|
|
50
|
+
args.verbose = true;
|
|
51
|
+
}
|
|
52
|
+
else if (a === "--help" || a === "-h") {
|
|
53
|
+
printHelp();
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!args.story) {
|
|
58
|
+
console.error("--story <id> is required.");
|
|
59
|
+
printHelp();
|
|
60
|
+
process.exit(64);
|
|
61
|
+
}
|
|
62
|
+
if (!args.outPath) {
|
|
63
|
+
args.outPath = join(args.repoRoot, ".brewing/recon-result.json");
|
|
64
|
+
}
|
|
65
|
+
return args;
|
|
66
|
+
}
|
|
67
|
+
function printHelp() {
|
|
68
|
+
console.log(`
|
|
69
|
+
slowcook recon — pre-brew structural divergence check (0.17.6+)
|
|
70
|
+
|
|
71
|
+
Compares the story's test files against the mock + src/ tree. Detects:
|
|
72
|
+
- Tests importing non-existent components (vibe ⇄ testgen name drift)
|
|
73
|
+
- Tests asserting on testids the mock doesn't render
|
|
74
|
+
- Renames that would break OTHER stories' tests (brownfield safety)
|
|
75
|
+
|
|
76
|
+
Usage:
|
|
77
|
+
slowcook recon --story <id> [--cwd <path>] [--out <path>] [--verbose]
|
|
78
|
+
|
|
79
|
+
Options:
|
|
80
|
+
--story <id> Story id (e.g. 017). Required.
|
|
81
|
+
--cwd <path> Repo root (default: cwd).
|
|
82
|
+
--out <path> Output JSON path (default: .brewing/recon-result.json).
|
|
83
|
+
--verbose Print detailed breakdown to stdout.
|
|
84
|
+
|
|
85
|
+
Exit codes:
|
|
86
|
+
0 status=clean OR status=rename_needed (recommendations only)
|
|
87
|
+
2 status=escalate (STORY_HISTORY_CONFLICT or VIBE_RECIPE_NAME_DRIFT)
|
|
88
|
+
`);
|
|
89
|
+
}
|
|
90
|
+
export async function recon(argv, _cliVersion) {
|
|
91
|
+
const args = parseArgs(argv);
|
|
92
|
+
console.log(`slowcook recon · story-${args.story} · cwd: ${relative(process.cwd(), args.repoRoot) || "."}`);
|
|
93
|
+
const idx = buildHistoryIndex({ repoRoot: args.repoRoot });
|
|
94
|
+
const result = {
|
|
95
|
+
story: args.story,
|
|
96
|
+
generated_at: new Date().toISOString(),
|
|
97
|
+
generator: "slowcook-recon@0.17.6",
|
|
98
|
+
status: "clean",
|
|
99
|
+
renames: [],
|
|
100
|
+
testid_gaps: [],
|
|
101
|
+
structural_gaps: [],
|
|
102
|
+
history_index_components: idx.components.length,
|
|
103
|
+
warnings: [],
|
|
104
|
+
};
|
|
105
|
+
// Find this story's test files
|
|
106
|
+
const testFiles = findStoryTestFiles(args.repoRoot, args.story);
|
|
107
|
+
if (testFiles.length === 0) {
|
|
108
|
+
result.warnings.push(`No test files found for story-${args.story} under tests/integration/`);
|
|
109
|
+
}
|
|
110
|
+
for (const testRel of testFiles) {
|
|
111
|
+
const testAbs = join(args.repoRoot, testRel);
|
|
112
|
+
const body = readFileSync(testAbs, "utf8");
|
|
113
|
+
const imports = extractImports(body);
|
|
114
|
+
const testids = extractTestids(body);
|
|
115
|
+
// Check 1: every test import → file exists somewhere reachable
|
|
116
|
+
for (const imp of imports) {
|
|
117
|
+
const resolved = resolveImport(args.repoRoot, imp);
|
|
118
|
+
if (!resolved) {
|
|
119
|
+
// Not in src/ or mock/ — it's a missing component
|
|
120
|
+
// Search history-index for a near-match by name
|
|
121
|
+
const compName = imp.split("/").pop() ?? "";
|
|
122
|
+
const nearMatch = idx.components.find((c) => c.name === compName);
|
|
123
|
+
if (nearMatch) {
|
|
124
|
+
result.structural_gaps.push({
|
|
125
|
+
kind: "missing_component",
|
|
126
|
+
test: testRel,
|
|
127
|
+
detail: `Test imports "${imp}" — file not found. Closest match in history-index: ${nearMatch.name} at ${nearMatch.file}.`,
|
|
128
|
+
recommendation: `Either rename mock/src/components to match "${compName}" OR /refine to use the existing name.`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
result.structural_gaps.push({
|
|
133
|
+
kind: "missing_component",
|
|
134
|
+
test: testRel,
|
|
135
|
+
detail: `Test imports "${imp}" — file not found anywhere. No near-match in history-index.`,
|
|
136
|
+
recommendation: `Either vibe should add this component OR /refine to drop the assertion.`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Check 2: testid selectors found in tests → assert mock has them
|
|
142
|
+
// (Light check — full check requires DOM render; this just greps mock files for the testid string.)
|
|
143
|
+
for (const tid of testids) {
|
|
144
|
+
const present = testidPresentInMock(args.repoRoot, tid);
|
|
145
|
+
if (!present) {
|
|
146
|
+
const existing = result.testid_gaps.find((g) => g.selector === tid);
|
|
147
|
+
if (existing) {
|
|
148
|
+
if (!existing.queried_by.includes(testRel))
|
|
149
|
+
existing.queried_by.push(testRel);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
result.testid_gaps.push({ selector: tid, queried_by: [testRel], in_mock: false });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Check 3: brownfield safety — for each component name in mock that
|
|
158
|
+
// doesn't appear in src/, check whether RENAMING the mock would break
|
|
159
|
+
// a different story's tests.
|
|
160
|
+
// (This is a heavier check; skip for v1. Recorded as a TODO.)
|
|
161
|
+
result.warnings.push("brownfield-rename-safety check is not yet implemented in 0.17.6 (recorded by simulation; defer to 0.17.7)");
|
|
162
|
+
// Decide status
|
|
163
|
+
if (result.structural_gaps.length > 0) {
|
|
164
|
+
result.status = "escalate";
|
|
165
|
+
}
|
|
166
|
+
else if (result.testid_gaps.length > 0 || result.renames.length > 0) {
|
|
167
|
+
result.status = "rename_needed";
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
result.status = "clean";
|
|
171
|
+
}
|
|
172
|
+
// Write output
|
|
173
|
+
mkdirSync(dirname(args.outPath), { recursive: true });
|
|
174
|
+
writeFileSync(args.outPath, JSON.stringify(result, null, 2), "utf8");
|
|
175
|
+
// Print summary
|
|
176
|
+
console.log(` status: ${result.status.toUpperCase()}`);
|
|
177
|
+
console.log(` ${testFiles.length} test file(s) · ${result.structural_gaps.length} structural gap(s) · ${result.testid_gaps.length} testid gap(s) · ${result.renames.length} rename(s)`);
|
|
178
|
+
if (args.verbose || result.status === "escalate") {
|
|
179
|
+
for (const g of result.structural_gaps) {
|
|
180
|
+
console.log(` ! ${g.kind}: ${g.detail}`);
|
|
181
|
+
console.log(` → ${g.recommendation}`);
|
|
182
|
+
}
|
|
183
|
+
for (const t of result.testid_gaps) {
|
|
184
|
+
console.log(` ? testid "${t.selector}" missing in mock; queried by ${t.queried_by.length} test(s)`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
console.log(` wrote ${relative(args.repoRoot, args.outPath) || args.outPath}`);
|
|
188
|
+
if (result.status === "escalate") {
|
|
189
|
+
console.error("\nrecon escalation: structural gaps prevent brew from converging. Fix before dispatching brew.");
|
|
190
|
+
process.exit(2);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ----- helpers -----
|
|
194
|
+
export function findStoryTestFiles(repoRoot, story) {
|
|
195
|
+
const dir = join(repoRoot, "tests/integration");
|
|
196
|
+
if (!existsSync(dir))
|
|
197
|
+
return [];
|
|
198
|
+
const out = [];
|
|
199
|
+
for (const name of readdirSync(dir)) {
|
|
200
|
+
if (!name.startsWith(`story-${story}`))
|
|
201
|
+
continue;
|
|
202
|
+
if (!/\.test\.(ts|tsx)$/.test(name))
|
|
203
|
+
continue;
|
|
204
|
+
out.push(`tests/integration/${name}`);
|
|
205
|
+
}
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
export function extractImports(body) {
|
|
209
|
+
const out = [];
|
|
210
|
+
for (const m of body.matchAll(/import[^"']*from\s+["']([^"']+)["']/g)) {
|
|
211
|
+
if (m[1] && (m[1].startsWith("@/") || m[1].startsWith(".")))
|
|
212
|
+
out.push(m[1]);
|
|
213
|
+
}
|
|
214
|
+
return [...new Set(out)];
|
|
215
|
+
}
|
|
216
|
+
export function extractTestids(body) {
|
|
217
|
+
const out = [];
|
|
218
|
+
for (const m of body.matchAll(/data-testid\s*=\s*["']([^"']+)["']/g)) {
|
|
219
|
+
if (m[1])
|
|
220
|
+
out.push(m[1]);
|
|
221
|
+
}
|
|
222
|
+
for (const m of body.matchAll(/getByTestId\s*\(\s*["']([^"']+)["']/g)) {
|
|
223
|
+
if (m[1])
|
|
224
|
+
out.push(m[1]);
|
|
225
|
+
}
|
|
226
|
+
for (const m of body.matchAll(/data-testid="([^"]+)"/g)) {
|
|
227
|
+
if (m[1])
|
|
228
|
+
out.push(m[1]);
|
|
229
|
+
}
|
|
230
|
+
return [...new Set(out)];
|
|
231
|
+
}
|
|
232
|
+
function resolveImport(repoRoot, imp) {
|
|
233
|
+
// @/foo → src/foo or mock/src/foo
|
|
234
|
+
if (imp.startsWith("@/")) {
|
|
235
|
+
const rel = imp.slice(2);
|
|
236
|
+
const candidates = [
|
|
237
|
+
`src/${rel}.ts`,
|
|
238
|
+
`src/${rel}.tsx`,
|
|
239
|
+
`src/${rel}/index.ts`,
|
|
240
|
+
`src/${rel}/index.tsx`,
|
|
241
|
+
`mock/src/${rel}.ts`,
|
|
242
|
+
`mock/src/${rel}.tsx`,
|
|
243
|
+
`mock/src/${rel}/index.ts`,
|
|
244
|
+
`mock/src/${rel}/index.tsx`,
|
|
245
|
+
];
|
|
246
|
+
for (const c of candidates) {
|
|
247
|
+
if (existsSync(join(repoRoot, c)))
|
|
248
|
+
return c;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
function testidPresentInMock(repoRoot, testid) {
|
|
254
|
+
// Walk mock/src/ + look for the literal testid string
|
|
255
|
+
const dir = join(repoRoot, "mock/src");
|
|
256
|
+
if (!existsSync(dir))
|
|
257
|
+
return false;
|
|
258
|
+
return walkAndGrep(dir, testid);
|
|
259
|
+
}
|
|
260
|
+
function walkAndGrep(dir, needle) {
|
|
261
|
+
for (const name of readdirSync(dir)) {
|
|
262
|
+
if (name.startsWith(".") || name === "node_modules")
|
|
263
|
+
continue;
|
|
264
|
+
const full = join(dir, name);
|
|
265
|
+
const st = statSync(full);
|
|
266
|
+
if (st.isDirectory()) {
|
|
267
|
+
if (walkAndGrep(full, needle))
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
else if (/\.(tsx?|jsx?)$/.test(name)) {
|
|
271
|
+
const body = readFileSync(full, "utf8");
|
|
272
|
+
if (body.includes(`"${needle}"`) || body.includes(`'${needle}'`))
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
//# sourceMappingURL=index.js.map
|