hatchkit 0.1.40 → 0.1.42
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/adopt.d.ts.map +1 -1
- package/dist/adopt.js +663 -82
- package/dist/adopt.js.map +1 -1
- package/dist/config.d.ts +32 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +91 -38
- package/dist/config.js.map +1 -1
- package/dist/deploy/coolify-app.d.ts.map +1 -1
- package/dist/deploy/coolify-app.js +0 -7
- package/dist/deploy/coolify-app.js.map +1 -1
- package/dist/deploy/coolify.d.ts.map +1 -1
- package/dist/deploy/coolify.js +20 -1
- package/dist/deploy/coolify.js.map +1 -1
- package/dist/deploy/ghcr.d.ts +4 -2
- package/dist/deploy/ghcr.d.ts.map +1 -1
- package/dist/deploy/ghcr.js +1 -1
- package/dist/deploy/ghcr.js.map +1 -1
- package/dist/deploy/github.d.ts +4 -3
- package/dist/deploy/github.d.ts.map +1 -1
- package/dist/deploy/github.js +5 -2
- package/dist/deploy/github.js.map +1 -1
- package/dist/deploy/pages.d.ts +41 -0
- package/dist/deploy/pages.d.ts.map +1 -1
- package/dist/deploy/pages.js +363 -22
- package/dist/deploy/pages.js.map +1 -1
- package/dist/deploy/regen-infra.d.ts.map +1 -1
- package/dist/deploy/regen-infra.js +5 -11
- package/dist/deploy/regen-infra.js.map +1 -1
- package/dist/deploy/rollback.d.ts.map +1 -1
- package/dist/deploy/rollback.js +44 -6
- package/dist/deploy/rollback.js.map +1 -1
- package/dist/deploy/terraform.d.ts.map +1 -1
- package/dist/deploy/terraform.js +20 -37
- package/dist/deploy/terraform.js.map +1 -1
- package/dist/dns.d.ts.map +1 -1
- package/dist/dns.js +4 -5
- package/dist/dns.js.map +1 -1
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +110 -36
- package/dist/doctor.js.map +1 -1
- package/dist/email/index.d.ts +31 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +251 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/presets.d.ts +14 -0
- package/dist/email/presets.d.ts.map +1 -0
- package/dist/email/presets.js +33 -0
- package/dist/email/presets.js.map +1 -0
- package/dist/email/setup.d.ts +93 -0
- package/dist/email/setup.d.ts.map +1 -0
- package/dist/email/setup.js +263 -0
- package/dist/email/setup.js.map +1 -0
- package/dist/email/spf.d.ts +56 -0
- package/dist/email/spf.d.ts.map +1 -0
- package/dist/email/spf.js +102 -0
- package/dist/email/spf.js.map +1 -0
- package/dist/index.js +306 -22
- package/dist/index.js.map +1 -1
- package/dist/inventory.d.ts +37 -0
- package/dist/inventory.d.ts.map +1 -1
- package/dist/inventory.js +536 -55
- package/dist/inventory.js.map +1 -1
- package/dist/overview.d.ts +101 -0
- package/dist/overview.d.ts.map +1 -0
- package/dist/overview.js +880 -0
- package/dist/overview.js.map +1 -0
- package/dist/prompts.d.ts +27 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +262 -34
- package/dist/prompts.js.map +1 -1
- package/dist/provision/index.d.ts +20 -1
- package/dist/provision/index.d.ts.map +1 -1
- package/dist/provision/index.js +115 -0
- package/dist/provision/index.js.map +1 -1
- package/dist/provision/s3-buckets.js +1 -1
- package/dist/provision/s3-buckets.js.map +1 -1
- package/dist/scaffold/app.d.ts.map +1 -1
- package/dist/scaffold/app.js +15 -7
- package/dist/scaffold/app.js.map +1 -1
- package/dist/scaffold/build-pipeline.d.ts +16 -0
- package/dist/scaffold/build-pipeline.d.ts.map +1 -1
- package/dist/scaffold/build-pipeline.js +47 -4
- package/dist/scaffold/build-pipeline.js.map +1 -1
- package/dist/scaffold/infra.d.ts +4 -5
- package/dist/scaffold/infra.d.ts.map +1 -1
- package/dist/scaffold/infra.js +18 -57
- package/dist/scaffold/infra.js.map +1 -1
- package/dist/scaffold/manifest.d.ts +6 -0
- package/dist/scaffold/manifest.d.ts.map +1 -1
- package/dist/scaffold/manifest.js +2 -0
- package/dist/scaffold/manifest.js.map +1 -1
- package/dist/scaffold/pages-heuristics.d.ts +17 -0
- package/dist/scaffold/pages-heuristics.d.ts.map +1 -0
- package/dist/scaffold/pages-heuristics.js +344 -0
- package/dist/scaffold/pages-heuristics.js.map +1 -0
- package/dist/scaffold/pages-mode.d.ts +10 -0
- package/dist/scaffold/pages-mode.d.ts.map +1 -0
- package/dist/scaffold/pages-mode.js +107 -0
- package/dist/scaffold/pages-mode.js.map +1 -0
- package/dist/scaffold/pkg-json.d.ts +4 -0
- package/dist/scaffold/pkg-json.d.ts.map +1 -1
- package/dist/scaffold/pkg-json.js +17 -0
- package/dist/scaffold/pkg-json.js.map +1 -1
- package/dist/scaffold/surfaces.d.ts.map +1 -1
- package/dist/scaffold/surfaces.js +12 -1
- package/dist/scaffold/surfaces.js.map +1 -1
- package/dist/scaffold/update.js +1 -1
- package/dist/scaffold/update.js.map +1 -1
- package/dist/templates/build-pipeline/Dockerfile.nextjs.hbs +103 -0
- package/dist/templates/build-pipeline/docker-compose.yml.hbs +23 -6
- package/dist/utils/cloudflare-api.d.ts +158 -13
- package/dist/utils/cloudflare-api.d.ts.map +1 -1
- package/dist/utils/cloudflare-api.js +219 -11
- package/dist/utils/cloudflare-api.js.map +1 -1
- package/dist/utils/coolify-api.d.ts +9 -0
- package/dist/utils/coolify-api.d.ts.map +1 -1
- package/dist/utils/coolify-api.js +26 -0
- package/dist/utils/coolify-api.js.map +1 -1
- package/dist/utils/run-ledger.d.ts +42 -1
- package/dist/utils/run-ledger.d.ts.map +1 -1
- package/dist/utils/run-ledger.js.map +1 -1
- package/dist/utils/s3-admin.d.ts +9 -0
- package/dist/utils/s3-admin.d.ts.map +1 -0
- package/dist/utils/s3-admin.js +46 -0
- package/dist/utils/s3-admin.js.map +1 -0
- package/package.json +1 -1
package/dist/inventory.js
CHANGED
|
@@ -23,16 +23,17 @@
|
|
|
23
23
|
*
|
|
24
24
|
* Everything is read-only. No mutations. Safe to run anywhere.
|
|
25
25
|
*/
|
|
26
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
26
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
27
27
|
import { join, resolve } from "node:path";
|
|
28
28
|
import { confirm, input } from "@inquirer/prompts";
|
|
29
29
|
import chalk from "chalk";
|
|
30
30
|
import { getCoolifyConfig, getDnsConfig, getGlitchtipConfig, getOpenpanelConfig, getResendConfig, getS3Config, getStripeConfig, } from "./config.js";
|
|
31
31
|
import { locateEnvKeysFile, locateEnvProductionFile } from "./deploy/keys.js";
|
|
32
|
-
import { MANIFEST_FILENAME, readManifest } from "./scaffold/manifest.js";
|
|
32
|
+
import { MANIFEST_FILENAME, MANIFEST_VERSION, readManifest, } from "./scaffold/manifest.js";
|
|
33
33
|
import { CloudflareApi } from "./utils/cloudflare-api.js";
|
|
34
34
|
import { CoolifyApi } from "./utils/coolify-api.js";
|
|
35
35
|
import { exec, execOk } from "./utils/exec.js";
|
|
36
|
+
import { listS3Buckets } from "./utils/s3-admin.js";
|
|
36
37
|
import { SECRET_KEYS, getSecret } from "./utils/secrets.js";
|
|
37
38
|
import { getCliVersion } from "./utils/version.js";
|
|
38
39
|
export async function runInventory(cwd, opts = {}) {
|
|
@@ -42,10 +43,40 @@ export async function runInventory(cwd, opts = {}) {
|
|
|
42
43
|
autoAccept: opts.yes ?? false,
|
|
43
44
|
});
|
|
44
45
|
if (opts.json) {
|
|
45
|
-
|
|
46
|
+
// Sets serialize as `{}` by default — surface them as arrays so
|
|
47
|
+
// JSON consumers can actually read the detected signals.
|
|
48
|
+
console.log(JSON.stringify(report, (_k, v) => (v instanceof Set ? Array.from(v).sort() : v), 2));
|
|
46
49
|
return;
|
|
47
50
|
}
|
|
48
51
|
console.log(renderInventoryHuman(report));
|
|
52
|
+
// Persist inferred identity as a minimal `.hatchkit.json` unless
|
|
53
|
+
// suppressed. Skip when there's already a manifest (adopt owns it
|
|
54
|
+
// — we don't overwrite), when the inputs aren't sufficient (name +
|
|
55
|
+
// domain are required by the schema), or when `--no-save` was passed.
|
|
56
|
+
if (opts.noSave)
|
|
57
|
+
return;
|
|
58
|
+
if (report.local.manifestPresent)
|
|
59
|
+
return;
|
|
60
|
+
if (!report.inferred.name || !report.inferred.domain)
|
|
61
|
+
return;
|
|
62
|
+
const absCwd = resolve(cwd);
|
|
63
|
+
const writeIt = () => {
|
|
64
|
+
writeMinimalManifest(absCwd, report.inferred, report.local);
|
|
65
|
+
console.log(chalk.dim(` Wrote minimal ${MANIFEST_FILENAME}. Run \`hatchkit adopt --resume\` to wire features + deploy config.`));
|
|
66
|
+
};
|
|
67
|
+
if (opts.save) {
|
|
68
|
+
writeIt();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Only ask in interactive mode (TTY + not --yes).
|
|
72
|
+
if (!process.stdin.isTTY || opts.yes)
|
|
73
|
+
return;
|
|
74
|
+
const ok = await confirm({
|
|
75
|
+
message: `Save inferred identity as a minimal ${MANIFEST_FILENAME}? (Run \`hatchkit adopt --resume\` afterwards to flesh out features + deploy config.)`,
|
|
76
|
+
default: true,
|
|
77
|
+
});
|
|
78
|
+
if (ok)
|
|
79
|
+
writeIt();
|
|
49
80
|
}
|
|
50
81
|
export async function collectInventory(cwd, opts = {}) {
|
|
51
82
|
const absCwd = resolve(cwd);
|
|
@@ -67,19 +98,25 @@ export async function collectInventory(cwd, opts = {}) {
|
|
|
67
98
|
if (opts.interactive) {
|
|
68
99
|
identity = await promptForGaps(local, inferred, sources, !!opts.autoAccept);
|
|
69
100
|
}
|
|
101
|
+
// Per-provider expectation flags — drives which `missing` findings
|
|
102
|
+
// surface as red ✗ vs dim ·. A library with no Coolify-deploy signals
|
|
103
|
+
// gets dim · for "no Coolify app named foo" (expected absence), but a
|
|
104
|
+
// project whose .env.development declares STRIPE_* gets red ✗ if no
|
|
105
|
+
// matching webhook exists (genuine missing).
|
|
106
|
+
const expectations = computeExpectations(local, identity);
|
|
70
107
|
// Provider scans — every one is best-effort and returns its own
|
|
71
108
|
// findings + skip reason. Running them in parallel keeps wall-time
|
|
72
109
|
// close to the slowest single round-trip.
|
|
73
110
|
const scanResults = await Promise.all([
|
|
74
|
-
scanCoolify(identity),
|
|
75
|
-
scanDns(identity),
|
|
76
|
-
scanR2(identity, local.manifest),
|
|
111
|
+
scanCoolify(identity, expectations.coolify),
|
|
112
|
+
scanDns(identity, expectations.dns),
|
|
113
|
+
scanR2(identity, local.manifest, expectations.r2),
|
|
77
114
|
scanS3Other(identity),
|
|
78
|
-
scanGitHub(identity),
|
|
79
|
-
scanResend(identity),
|
|
80
|
-
scanGlitchtip(identity),
|
|
81
|
-
scanOpenpanel(identity),
|
|
82
|
-
scanStripe(identity),
|
|
115
|
+
scanGitHub(identity, { github: expectations.github, githubPages: expectations.githubPages }),
|
|
116
|
+
scanResend(identity, expectations.resend),
|
|
117
|
+
scanGlitchtip(identity, expectations.glitchtip),
|
|
118
|
+
scanOpenpanel(identity, expectations.openpanel),
|
|
119
|
+
scanStripe(identity, expectations.stripe),
|
|
83
120
|
]);
|
|
84
121
|
const findings = [];
|
|
85
122
|
const skipped = [];
|
|
@@ -94,7 +131,8 @@ export async function collectInventory(cwd, opts = {}) {
|
|
|
94
131
|
findings.push(...driftFindings);
|
|
95
132
|
const drifts = findings.filter((f) => f.status === "drift");
|
|
96
133
|
const present = findings.filter((f) => f.status === "present").length;
|
|
97
|
-
const
|
|
134
|
+
const missingAll = findings.filter((f) => f.status === "missing");
|
|
135
|
+
const expectedMissing = missingAll.filter((f) => f.expected).length;
|
|
98
136
|
return {
|
|
99
137
|
cliVersion: getCliVersion(),
|
|
100
138
|
cwd: absCwd,
|
|
@@ -104,7 +142,13 @@ export async function collectInventory(cwd, opts = {}) {
|
|
|
104
142
|
findings,
|
|
105
143
|
drifts,
|
|
106
144
|
skipped,
|
|
107
|
-
summary: {
|
|
145
|
+
summary: {
|
|
146
|
+
present,
|
|
147
|
+
drift: drifts.length,
|
|
148
|
+
missing: missingAll.length,
|
|
149
|
+
expectedMissing,
|
|
150
|
+
skipped: skipped.length,
|
|
151
|
+
},
|
|
108
152
|
};
|
|
109
153
|
}
|
|
110
154
|
// ---------------------------------------------------------------------------
|
|
@@ -250,6 +294,13 @@ function inferLocal(cwd) {
|
|
|
250
294
|
}
|
|
251
295
|
}
|
|
252
296
|
const envKeysPresent = !!locateEnvKeysFile(cwd);
|
|
297
|
+
// Provider expectation signals — read plaintext .env.* files and
|
|
298
|
+
// every package.json under the project for hints about which
|
|
299
|
+
// providers this project actually depends on. Used to mark `missing`
|
|
300
|
+
// findings as `expected: true` so the renderer only shows red ✗ when
|
|
301
|
+
// local state declares the resource should exist.
|
|
302
|
+
const envSignals = collectEnvSignals(cwd, serverDir, clientDir);
|
|
303
|
+
const packageDeps = collectPackageDeps(cwd, serverDir, clientDir);
|
|
253
304
|
// `.git` lives at the repo root — in a worktree it's a file pointing
|
|
254
305
|
// at the main repo, in a normal clone it's a directory. Either form
|
|
255
306
|
// is fine for existsSync. Walk up from cwd so running `hatchkit
|
|
@@ -273,7 +324,131 @@ function inferLocal(cwd) {
|
|
|
273
324
|
cnameFile,
|
|
274
325
|
dotenvxEncrypted,
|
|
275
326
|
envKeysPresent,
|
|
327
|
+
envSignals,
|
|
328
|
+
packageDeps,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// Expectation-signal collectors (used to mark findings as `expected`)
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
/** Walk plaintext .env.example / .env.development files at the project
|
|
335
|
+
* root and any detected server/client dirs, returning the set of
|
|
336
|
+
* env-var-name prefixes encountered. Encrypted .env.production is
|
|
337
|
+
* intentionally NOT decrypted — we read declarative templates only. */
|
|
338
|
+
function collectEnvSignals(cwd, serverDir, clientDir) {
|
|
339
|
+
const signals = new Set();
|
|
340
|
+
const filenames = [".env.example", ".env.development", ".env"];
|
|
341
|
+
const dirs = [cwd, serverDir, clientDir].filter((d) => !!d);
|
|
342
|
+
// Patterns we recognize — prefix → signal name. Keep this list
|
|
343
|
+
// tight; over-broad matches lead to spurious "expected" flags.
|
|
344
|
+
const patterns = [
|
|
345
|
+
{ re: /^\s*RESEND_/m, signal: "RESEND" },
|
|
346
|
+
{ re: /^\s*GLITCHTIP_DSN|^\s*PUBLIC_GLITCHTIP_DSN/m, signal: "GLITCHTIP" },
|
|
347
|
+
{ re: /^\s*SENTRY_DSN|^\s*PUBLIC_SENTRY_DSN/m, signal: "SENTRY" },
|
|
348
|
+
{ re: /^\s*OPENPANEL_|^\s*PUBLIC_OPENPANEL_/m, signal: "OPENPANEL" },
|
|
349
|
+
{ re: /^\s*STRIPE_/m, signal: "STRIPE" },
|
|
350
|
+
{ re: /^\s*R2_/m, signal: "R2" },
|
|
351
|
+
{ re: /^\s*S3_|^\s*AWS_(ACCESS_KEY_ID|SECRET_ACCESS_KEY|REGION)/m, signal: "S3" },
|
|
352
|
+
];
|
|
353
|
+
for (const dir of dirs) {
|
|
354
|
+
for (const name of filenames) {
|
|
355
|
+
const p = join(dir, name);
|
|
356
|
+
if (!existsSync(p))
|
|
357
|
+
continue;
|
|
358
|
+
let body;
|
|
359
|
+
try {
|
|
360
|
+
body = readFileSync(p, "utf-8");
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
// Strip commented-out lines — `# RESEND_API_KEY=...` shouldn't
|
|
366
|
+
// count as a signal that the project uses Resend.
|
|
367
|
+
const live = body
|
|
368
|
+
.split("\n")
|
|
369
|
+
.filter((l) => !/^\s*#/.test(l))
|
|
370
|
+
.join("\n");
|
|
371
|
+
for (const { re, signal } of patterns) {
|
|
372
|
+
if (re.test(live))
|
|
373
|
+
signals.add(signal);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return signals;
|
|
378
|
+
}
|
|
379
|
+
/** Read every package.json under the project (root + server/client
|
|
380
|
+
* dirs) and return the union of declared dependency names (deps +
|
|
381
|
+
* devDeps + peerDeps). Cheap heuristic — we don't follow workspace
|
|
382
|
+
* globs, just probe the conventional monorepo locations. */
|
|
383
|
+
function collectPackageDeps(cwd, serverDir, clientDir) {
|
|
384
|
+
const out = new Set();
|
|
385
|
+
const dirs = [cwd, serverDir, clientDir].filter((d) => !!d);
|
|
386
|
+
for (const dir of dirs) {
|
|
387
|
+
const p = join(dir, "package.json");
|
|
388
|
+
if (!existsSync(p))
|
|
389
|
+
continue;
|
|
390
|
+
try {
|
|
391
|
+
const pkg = JSON.parse(readFileSync(p, "utf-8"));
|
|
392
|
+
for (const block of [pkg.dependencies, pkg.devDependencies, pkg.peerDependencies]) {
|
|
393
|
+
if (!block)
|
|
394
|
+
continue;
|
|
395
|
+
for (const name of Object.keys(block))
|
|
396
|
+
out.add(name);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// Unparseable package.json — skip.
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return out;
|
|
404
|
+
}
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
// Minimal `.hatchkit.json` writer
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
//
|
|
409
|
+
// When inventory saves inferred identity it writes a minimal manifest
|
|
410
|
+
// that follows the full schema (so every other hatchkit command — adopt,
|
|
411
|
+
// update, sync, keys — can read it) but with conservative defaults for
|
|
412
|
+
// fields inventory can't reliably infer. Run `hatchkit adopt --resume`
|
|
413
|
+
// afterwards to flesh out features, surfaces, ports, etc. — adopt's
|
|
414
|
+
// stepper reads this manifest as its starting state.
|
|
415
|
+
export function writeMinimalManifest(cwd, identity, local) {
|
|
416
|
+
if (!identity.name || !identity.domain) {
|
|
417
|
+
throw new Error("Can't write .hatchkit.json without an inferred name and domain. " +
|
|
418
|
+
"Pass --name / --domain or run inventory interactively to provide them.");
|
|
419
|
+
}
|
|
420
|
+
// Surfaces from local layout — both dirs → "both", just one → that
|
|
421
|
+
// one, neither → fall back to "both" (most common scaffold shape).
|
|
422
|
+
const surfaces = local.serverDir && local.clientDir
|
|
423
|
+
? "both"
|
|
424
|
+
: local.serverDir
|
|
425
|
+
? "server-only"
|
|
426
|
+
: local.clientDir
|
|
427
|
+
? "client-only"
|
|
428
|
+
: "both";
|
|
429
|
+
const manifest = {
|
|
430
|
+
version: MANIFEST_VERSION,
|
|
431
|
+
cliVersion: getCliVersion(),
|
|
432
|
+
scaffoldedAt: new Date().toISOString(),
|
|
433
|
+
name: identity.name,
|
|
434
|
+
domain: identity.domain,
|
|
435
|
+
features: [],
|
|
436
|
+
mlServices: [],
|
|
437
|
+
// "none" is the conservative default — even if local deps signal R2
|
|
438
|
+
// usage, we don't claim ownership of buckets we haven't provisioned.
|
|
439
|
+
// `hatchkit adopt --resume` will prompt for the real value.
|
|
440
|
+
s3Provider: "none",
|
|
441
|
+
deployTarget: "existing",
|
|
442
|
+
surfaces,
|
|
443
|
+
// Conventional defaults — `hatchkit adopt --resume` lets the user
|
|
444
|
+
// override if the project actually uses different ports.
|
|
445
|
+
ports: { server: 3000, client: 5173 },
|
|
276
446
|
};
|
|
447
|
+
writeManifestFile(cwd, manifest);
|
|
448
|
+
return manifest;
|
|
449
|
+
}
|
|
450
|
+
function writeManifestFile(cwd, manifest) {
|
|
451
|
+
writeFileSync(join(cwd, MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
|
|
277
452
|
}
|
|
278
453
|
function findGitRoot(startDir) {
|
|
279
454
|
let dir = startDir;
|
|
@@ -306,7 +481,8 @@ function firstExistingDir(root, rels) {
|
|
|
306
481
|
function inferIdentity(local, override) {
|
|
307
482
|
const sources = {};
|
|
308
483
|
const out = {};
|
|
309
|
-
// name: flag > manifest > package.json > basename(cwd)
|
|
484
|
+
// name: flag > manifest > identity-file > package.json > basename(cwd)
|
|
485
|
+
// name: flag > manifest > package.json > basename(cwd)
|
|
310
486
|
if (override.name) {
|
|
311
487
|
out.name = override.name;
|
|
312
488
|
sources.name = "flag";
|
|
@@ -329,10 +505,9 @@ function inferIdentity(local, override) {
|
|
|
329
505
|
sources.name = "cwd-basename";
|
|
330
506
|
}
|
|
331
507
|
}
|
|
332
|
-
// domain: flag > manifest > CNAME file
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
// domain patterns against any zone we list.
|
|
508
|
+
// domain: flag > manifest > CNAME file. We deliberately don't derive
|
|
509
|
+
// a domain from the project name — too speculative, and the matching
|
|
510
|
+
// layer already tries common domain patterns against any zone we list.
|
|
336
511
|
if (override.domain) {
|
|
337
512
|
out.domain = override.domain;
|
|
338
513
|
sources.domain = "flag";
|
|
@@ -345,15 +520,11 @@ function inferIdentity(local, override) {
|
|
|
345
520
|
out.domain = local.cnameFile.content;
|
|
346
521
|
sources.domain = "cname-file";
|
|
347
522
|
}
|
|
348
|
-
// repo: flag
|
|
523
|
+
// repo: flag (else filled in by caller from git remote)
|
|
349
524
|
if (override.repo) {
|
|
350
525
|
out.repo = override.repo;
|
|
351
526
|
sources.repo = "flag";
|
|
352
527
|
}
|
|
353
|
-
else {
|
|
354
|
-
// git remote is resolved async — leave undefined here; the caller
|
|
355
|
-
// fills it via resolveGitRemote (run before prompting).
|
|
356
|
-
}
|
|
357
528
|
return { input: out, sources };
|
|
358
529
|
}
|
|
359
530
|
async function resolveGitRemote(local) {
|
|
@@ -522,7 +693,46 @@ function rel(cwd, abs) {
|
|
|
522
693
|
return abs.slice(cwd.length + 1);
|
|
523
694
|
return abs;
|
|
524
695
|
}
|
|
525
|
-
|
|
696
|
+
export function computeExpectations(local, identity) {
|
|
697
|
+
const env = local.envSignals;
|
|
698
|
+
const deps = local.packageDeps;
|
|
699
|
+
const hasManifestBuckets = (() => {
|
|
700
|
+
const b = local.manifest?.s3Buckets;
|
|
701
|
+
if (!b)
|
|
702
|
+
return false;
|
|
703
|
+
return Object.values(b).some((v) => v && typeof v === "object");
|
|
704
|
+
})();
|
|
705
|
+
return {
|
|
706
|
+
// Coolify is the project's deploy target whenever there's a manifest
|
|
707
|
+
// (every hatchkit-scaffolded project deploys there) or a deploy
|
|
708
|
+
// workflow committed.
|
|
709
|
+
coolify: !!local.manifest || !!local.deployWorkflowPath,
|
|
710
|
+
// DNS is always relevant when we know a domain — the user wouldn't
|
|
711
|
+
// have a domain unless they intended to route something to it.
|
|
712
|
+
dns: !!identity.domain,
|
|
713
|
+
r2: hasManifestBuckets || env.has("R2") || env.has("S3") || deps.has("@aws-sdk/client-s3"),
|
|
714
|
+
github: !!identity.repo,
|
|
715
|
+
// Pages is expected when a deploy workflow is committed or the
|
|
716
|
+
// repo has a CNAME file at one of the conventional locations.
|
|
717
|
+
githubPages: !!local.ghPagesWorkflowPath || !!local.cnameFile,
|
|
718
|
+
resend: env.has("RESEND") || deps.has("resend"),
|
|
719
|
+
// The Sentry SDK works against GlitchTip (same wire protocol), so
|
|
720
|
+
// either signal counts. Same for an explicit GLITCHTIP_DSN.
|
|
721
|
+
glitchtip: env.has("GLITCHTIP") ||
|
|
722
|
+
env.has("SENTRY") ||
|
|
723
|
+
deps.has("glitchtip") ||
|
|
724
|
+
hasDepMatching(deps, /^@sentry\//),
|
|
725
|
+
openpanel: env.has("OPENPANEL") || hasDepMatching(deps, /^@openpanel\//) || deps.has("openpanel"),
|
|
726
|
+
stripe: env.has("STRIPE") || deps.has("stripe") || deps.has("@stripe/stripe-js"),
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
function hasDepMatching(deps, re) {
|
|
730
|
+
for (const d of deps)
|
|
731
|
+
if (re.test(d))
|
|
732
|
+
return true;
|
|
733
|
+
return false;
|
|
734
|
+
}
|
|
735
|
+
async function scanCoolify(input, expected) {
|
|
526
736
|
const provider = "coolify";
|
|
527
737
|
const findings = [];
|
|
528
738
|
const skipped = [];
|
|
@@ -602,6 +812,7 @@ async function scanCoolify(input) {
|
|
|
602
812
|
kind: "application",
|
|
603
813
|
identity: input.name,
|
|
604
814
|
status: "missing",
|
|
815
|
+
expected,
|
|
605
816
|
detail: `no Coolify project or app named ${wantedNames.join(" / ")} (${apps.length} app(s) total)`,
|
|
606
817
|
});
|
|
607
818
|
}
|
|
@@ -634,7 +845,7 @@ function collectFqdns(app) {
|
|
|
634
845
|
function nameAliases(name) {
|
|
635
846
|
return [name, `${name}-server`, `${name}-client`, `${name}-web`, `${name}-api`];
|
|
636
847
|
}
|
|
637
|
-
async function scanDns(input) {
|
|
848
|
+
async function scanDns(input, expected) {
|
|
638
849
|
const provider = "dns";
|
|
639
850
|
const findings = [];
|
|
640
851
|
const skipped = [];
|
|
@@ -677,6 +888,7 @@ async function scanDns(input) {
|
|
|
677
888
|
kind: "zone",
|
|
678
889
|
identity: apex,
|
|
679
890
|
status: "missing",
|
|
891
|
+
expected,
|
|
680
892
|
detail: "no Cloudflare zone for this apex",
|
|
681
893
|
});
|
|
682
894
|
return { provider, findings, skipped };
|
|
@@ -745,7 +957,7 @@ function relevantRecordProbes(input) {
|
|
|
745
957
|
}
|
|
746
958
|
return out;
|
|
747
959
|
}
|
|
748
|
-
async function scanR2(input, manifest) {
|
|
960
|
+
async function scanR2(input, manifest, expected) {
|
|
749
961
|
const provider = "s3:r2";
|
|
750
962
|
const findings = [];
|
|
751
963
|
const skipped = [];
|
|
@@ -839,39 +1051,62 @@ async function scanR2(input, manifest) {
|
|
|
839
1051
|
kind: "bucket",
|
|
840
1052
|
identity: input.name ?? "(candidates)",
|
|
841
1053
|
status: "missing",
|
|
1054
|
+
expected,
|
|
842
1055
|
detail: `no R2 bucket matches ${Array.from(candidates).join(" / ")} (account ${accountId.slice(0, 6)}…)`,
|
|
843
1056
|
});
|
|
844
1057
|
}
|
|
845
1058
|
return { provider, findings, skipped, raw: { accountId, live, manifestBuckets } };
|
|
846
1059
|
}
|
|
847
|
-
async function scanS3Other(
|
|
848
|
-
// Hetzner Object Storage + AWS S3
|
|
849
|
-
//
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
//
|
|
853
|
-
// ship without for now and revisit if anyone asks.
|
|
1060
|
+
async function scanS3Other(input) {
|
|
1061
|
+
// Account-wide `ListBuckets` over Hetzner Object Storage + AWS S3 via
|
|
1062
|
+
// the AWS SDK (already a dep — assets/mirror.ts uses it for the
|
|
1063
|
+
// streaming copy path). Emits one `present` finding per live bucket
|
|
1064
|
+
// plus a credentials-level info line so the user can spot a misrouted
|
|
1065
|
+
// endpoint without scrolling to drift.
|
|
854
1066
|
const provider = "s3";
|
|
855
1067
|
const findings = [];
|
|
856
1068
|
const skipped = [];
|
|
1069
|
+
const nameMatch = input.name?.toLowerCase();
|
|
857
1070
|
for (const p of ["hetzner", "aws"]) {
|
|
858
1071
|
const cfg = await getS3Config(p);
|
|
859
|
-
if (cfg) {
|
|
1072
|
+
if (!cfg) {
|
|
1073
|
+
skipped.push({ provider: `s3:${p}`, reason: "not configured" });
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
try {
|
|
1077
|
+
const buckets = await listS3Buckets(cfg);
|
|
860
1078
|
findings.push({
|
|
861
1079
|
provider: `s3:${p}`,
|
|
862
1080
|
kind: "credentials",
|
|
863
1081
|
identity: p,
|
|
864
1082
|
status: "info",
|
|
865
|
-
detail: `endpoint: ${cfg.endpoint}
|
|
1083
|
+
detail: `endpoint: ${cfg.endpoint} · ${buckets.length} bucket${buckets.length === 1 ? "" : "s"}`,
|
|
866
1084
|
});
|
|
1085
|
+
for (const b of buckets) {
|
|
1086
|
+
const matchesProject = nameMatch !== undefined &&
|
|
1087
|
+
(b.name.toLowerCase() === nameMatch || b.name.toLowerCase().startsWith(`${nameMatch}-`));
|
|
1088
|
+
findings.push({
|
|
1089
|
+
provider: `s3:${p}`,
|
|
1090
|
+
kind: "bucket",
|
|
1091
|
+
identity: b.name,
|
|
1092
|
+
status: "present",
|
|
1093
|
+
detail: b.creationDate
|
|
1094
|
+
? `created ${b.creationDate.toISOString().slice(0, 10)}`
|
|
1095
|
+
: undefined,
|
|
1096
|
+
expected: matchesProject || undefined,
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
867
1099
|
}
|
|
868
|
-
|
|
869
|
-
skipped.push({
|
|
1100
|
+
catch (err) {
|
|
1101
|
+
skipped.push({
|
|
1102
|
+
provider: `s3:${p}`,
|
|
1103
|
+
reason: `ListBuckets failed: ${err.message.split("\n")[0]}`,
|
|
1104
|
+
});
|
|
870
1105
|
}
|
|
871
1106
|
}
|
|
872
1107
|
return { provider, findings, skipped };
|
|
873
1108
|
}
|
|
874
|
-
async function scanGitHub(input) {
|
|
1109
|
+
async function scanGitHub(input, expects) {
|
|
875
1110
|
const provider = "github";
|
|
876
1111
|
const findings = [];
|
|
877
1112
|
const skipped = [];
|
|
@@ -920,6 +1155,7 @@ async function scanGitHub(input) {
|
|
|
920
1155
|
kind: "repository",
|
|
921
1156
|
identity: input.repo,
|
|
922
1157
|
status: "missing",
|
|
1158
|
+
expected: expects.github,
|
|
923
1159
|
detail: res.stderr.trim().split("\n")[0],
|
|
924
1160
|
});
|
|
925
1161
|
return { provider, findings, skipped, raw: { repoInfo } };
|
|
@@ -959,6 +1195,7 @@ async function scanGitHub(input) {
|
|
|
959
1195
|
kind: "page-site",
|
|
960
1196
|
identity: input.repo,
|
|
961
1197
|
status: "missing",
|
|
1198
|
+
expected: expects.githubPages,
|
|
962
1199
|
detail: "Pages is not enabled on this repo",
|
|
963
1200
|
});
|
|
964
1201
|
}
|
|
@@ -1019,7 +1256,7 @@ async function scanGitHub(input) {
|
|
|
1019
1256
|
}
|
|
1020
1257
|
return { provider, findings, skipped, raw: { repoInfo, pages } };
|
|
1021
1258
|
}
|
|
1022
|
-
async function scanResend(input) {
|
|
1259
|
+
async function scanResend(input, expected) {
|
|
1023
1260
|
const provider = "resend";
|
|
1024
1261
|
const findings = [];
|
|
1025
1262
|
const skipped = [];
|
|
@@ -1047,6 +1284,7 @@ async function scanResend(input) {
|
|
|
1047
1284
|
kind: "verified-domain",
|
|
1048
1285
|
identity: input.domain,
|
|
1049
1286
|
status: "missing",
|
|
1287
|
+
expected,
|
|
1050
1288
|
detail: `no Resend domain entry for ${input.domain} (${(body.data ?? []).length} domain(s) total)`,
|
|
1051
1289
|
});
|
|
1052
1290
|
}
|
|
@@ -1070,7 +1308,7 @@ async function scanResend(input) {
|
|
|
1070
1308
|
}
|
|
1071
1309
|
return { provider, findings, skipped };
|
|
1072
1310
|
}
|
|
1073
|
-
async function scanGlitchtip(input) {
|
|
1311
|
+
async function scanGlitchtip(input, expected) {
|
|
1074
1312
|
const provider = "glitchtip";
|
|
1075
1313
|
const findings = [];
|
|
1076
1314
|
const skipped = [];
|
|
@@ -1097,6 +1335,7 @@ async function scanGlitchtip(input) {
|
|
|
1097
1335
|
kind: "project",
|
|
1098
1336
|
identity: input.name,
|
|
1099
1337
|
status: "missing",
|
|
1338
|
+
expected,
|
|
1100
1339
|
detail: `no GlitchTip project matching ${wanted.join(" / ")} (${body.length} total in org)`,
|
|
1101
1340
|
});
|
|
1102
1341
|
}
|
|
@@ -1120,7 +1359,7 @@ async function scanGlitchtip(input) {
|
|
|
1120
1359
|
}
|
|
1121
1360
|
return { provider, findings, skipped };
|
|
1122
1361
|
}
|
|
1123
|
-
async function scanOpenpanel(input) {
|
|
1362
|
+
async function scanOpenpanel(input, expected) {
|
|
1124
1363
|
const provider = "openpanel";
|
|
1125
1364
|
const findings = [];
|
|
1126
1365
|
const skipped = [];
|
|
@@ -1158,6 +1397,7 @@ async function scanOpenpanel(input) {
|
|
|
1158
1397
|
kind: "project",
|
|
1159
1398
|
identity: input.name,
|
|
1160
1399
|
status: "missing",
|
|
1400
|
+
expected,
|
|
1161
1401
|
detail: `no OpenPanel project matching ${wanted.join(" / ")} (${projects.length} total)`,
|
|
1162
1402
|
});
|
|
1163
1403
|
}
|
|
@@ -1180,7 +1420,7 @@ async function scanOpenpanel(input) {
|
|
|
1180
1420
|
}
|
|
1181
1421
|
return { provider, findings, skipped };
|
|
1182
1422
|
}
|
|
1183
|
-
async function scanStripe(input) {
|
|
1423
|
+
async function scanStripe(input, expected) {
|
|
1184
1424
|
const provider = "stripe";
|
|
1185
1425
|
const findings = [];
|
|
1186
1426
|
const skipped = [];
|
|
@@ -1215,6 +1455,7 @@ async function scanStripe(input) {
|
|
|
1215
1455
|
kind: "webhook-endpoint",
|
|
1216
1456
|
identity: `${mode} mode`,
|
|
1217
1457
|
status: "missing",
|
|
1458
|
+
expected,
|
|
1218
1459
|
detail: `no webhook endpoint with URL containing ${input.domain} (${(body.data ?? []).length} endpoint(s) in ${mode} mode)`,
|
|
1219
1460
|
});
|
|
1220
1461
|
}
|
|
@@ -1486,13 +1727,7 @@ export function renderInventoryHuman(report) {
|
|
|
1486
1727
|
continue;
|
|
1487
1728
|
lines.push(chalk.bold(` ${providerKey}`));
|
|
1488
1729
|
for (const f of findings) {
|
|
1489
|
-
const icon = f
|
|
1490
|
-
? chalk.green("✓")
|
|
1491
|
-
: f.status === "missing"
|
|
1492
|
-
? chalk.red("✗")
|
|
1493
|
-
: f.status === "drift"
|
|
1494
|
-
? chalk.yellow("⚠")
|
|
1495
|
-
: chalk.dim("·");
|
|
1730
|
+
const icon = findingIcon(f);
|
|
1496
1731
|
const kind = chalk.dim(`(${f.kind})`);
|
|
1497
1732
|
const detail = f.detail ? chalk.dim(` — ${f.detail}`) : "";
|
|
1498
1733
|
lines.push(` ${icon} ${f.identity} ${kind}${detail}`);
|
|
@@ -1506,12 +1741,10 @@ export function renderInventoryHuman(report) {
|
|
|
1506
1741
|
}
|
|
1507
1742
|
lines.push("");
|
|
1508
1743
|
}
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
: chalk.dim("0 missing")} ${chalk.dim(`${report.summary.skipped} skipped`)}`);
|
|
1514
|
-
lines.push("");
|
|
1744
|
+
// Final one-page summary — compact per-provider roll-up, no detail
|
|
1745
|
+
// fluff. The reader can scroll up for specifics; this block answers
|
|
1746
|
+
// "what does this project have, and what needs attention?" at a glance.
|
|
1747
|
+
lines.push(renderInventorySummary(report));
|
|
1515
1748
|
return lines.join("\n");
|
|
1516
1749
|
}
|
|
1517
1750
|
function sourceTag(s) {
|
|
@@ -1519,4 +1752,252 @@ function sourceTag(s) {
|
|
|
1519
1752
|
return chalk.dim(" (will prompt)");
|
|
1520
1753
|
return chalk.dim(` ← ${s}`);
|
|
1521
1754
|
}
|
|
1755
|
+
/** Status icon for a single finding. Red ✗ is reserved for `missing`
|
|
1756
|
+
* findings the project actually expects (declared in manifest, env,
|
|
1757
|
+
* package deps, workflows). An unexpected absence — e.g. "no Coolify
|
|
1758
|
+
* app named foo" for a library that doesn't deploy — is dim · instead. */
|
|
1759
|
+
function findingIcon(f) {
|
|
1760
|
+
if (f.status === "present")
|
|
1761
|
+
return chalk.green("✓");
|
|
1762
|
+
if (f.status === "drift")
|
|
1763
|
+
return chalk.yellow("⚠");
|
|
1764
|
+
if (f.status === "missing")
|
|
1765
|
+
return f.expected ? chalk.red("✗") : chalk.dim("·");
|
|
1766
|
+
return chalk.dim("·");
|
|
1767
|
+
}
|
|
1768
|
+
function renderInventorySummary(report) {
|
|
1769
|
+
const lines = [];
|
|
1770
|
+
lines.push(chalk.dim(` ${"─".repeat(58)}`));
|
|
1771
|
+
lines.push(chalk.bold(" Summary"));
|
|
1772
|
+
lines.push("");
|
|
1773
|
+
// Group findings by *effective* provider — drift findings get
|
|
1774
|
+
// re-attributed to the provider they're about, so each provider's
|
|
1775
|
+
// summary line reflects all its state (including drift).
|
|
1776
|
+
const grouped = new Map();
|
|
1777
|
+
for (const f of report.findings) {
|
|
1778
|
+
const effective = f.provider === "drift" ? driftToProvider(f.kind) : f.provider;
|
|
1779
|
+
const list = grouped.get(effective);
|
|
1780
|
+
if (list)
|
|
1781
|
+
list.push(f);
|
|
1782
|
+
else
|
|
1783
|
+
grouped.set(effective, [f]);
|
|
1784
|
+
}
|
|
1785
|
+
const rolls = [];
|
|
1786
|
+
for (const [providerKey, findings] of grouped) {
|
|
1787
|
+
rolls.push(rollUpProvider(providerKey, findings));
|
|
1788
|
+
}
|
|
1789
|
+
// Append skipped providers (not in grouped) as dim rows so the
|
|
1790
|
+
// summary lists every provider hatchkit knows about, not just the
|
|
1791
|
+
// ones that returned findings.
|
|
1792
|
+
const seen = new Set(grouped.keys());
|
|
1793
|
+
for (const s of report.skipped) {
|
|
1794
|
+
if (seen.has(s.provider))
|
|
1795
|
+
continue;
|
|
1796
|
+
seen.add(s.provider);
|
|
1797
|
+
rolls.push({
|
|
1798
|
+
label: providerLabel(s.provider),
|
|
1799
|
+
icon: chalk.dim("·"),
|
|
1800
|
+
text: chalk.dim(s.reason),
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
const labelWidth = Math.max(...rolls.map((r) => r.label.length), 10);
|
|
1804
|
+
for (const r of rolls) {
|
|
1805
|
+
lines.push(` ${r.label.padEnd(labelWidth + 2)} ${r.icon} ${r.text}`);
|
|
1806
|
+
}
|
|
1807
|
+
// Bottom-line takeaway — counts only `expected` missing (the
|
|
1808
|
+
// actionable subset). Unexpected absences ("no Coolify app named foo"
|
|
1809
|
+
// for a CLI library) are excluded; they're not problems to fix.
|
|
1810
|
+
lines.push("");
|
|
1811
|
+
const drift = report.summary.drift;
|
|
1812
|
+
const missing = report.summary.expectedMissing;
|
|
1813
|
+
const present = report.summary.present;
|
|
1814
|
+
if (drift > 0 || missing > 0) {
|
|
1815
|
+
const parts = [];
|
|
1816
|
+
if (drift > 0) {
|
|
1817
|
+
parts.push(chalk.yellow(`⚠ ${drift} drift${drift === 1 ? "" : "s"} to reconcile`));
|
|
1818
|
+
}
|
|
1819
|
+
if (missing > 0) {
|
|
1820
|
+
parts.push(chalk.red(`✗ ${missing} expected resource${missing === 1 ? "" : "s"} missing`));
|
|
1821
|
+
}
|
|
1822
|
+
lines.push(` ${parts.join(chalk.dim(" · "))}`);
|
|
1823
|
+
}
|
|
1824
|
+
else if (present > 0) {
|
|
1825
|
+
lines.push(` ${chalk.green("✓ All clear")} — ${present} resource${present === 1 ? "" : "s"} tracked, nothing out of sync.`);
|
|
1826
|
+
}
|
|
1827
|
+
else {
|
|
1828
|
+
lines.push(chalk.dim(" Nothing matched — try `--name`, `--domain`, or `--repo` to narrow."));
|
|
1829
|
+
}
|
|
1830
|
+
lines.push("");
|
|
1831
|
+
return lines.join("\n");
|
|
1832
|
+
}
|
|
1833
|
+
/** Drift findings live under provider "drift" in `report.findings`,
|
|
1834
|
+
* but conceptually they belong to the provider they describe. Pin
|
|
1835
|
+
* each drift `kind` to its source provider for the summary roll-up. */
|
|
1836
|
+
function driftToProvider(driftKind) {
|
|
1837
|
+
if (driftKind.startsWith("coolify"))
|
|
1838
|
+
return "coolify";
|
|
1839
|
+
if (driftKind === "bucket" || driftKind === "bucket-cors")
|
|
1840
|
+
return "s3:r2";
|
|
1841
|
+
if (driftKind.startsWith("github-pages"))
|
|
1842
|
+
return "github-pages";
|
|
1843
|
+
if (driftKind === "missing-secret")
|
|
1844
|
+
return "github";
|
|
1845
|
+
return "drift";
|
|
1846
|
+
}
|
|
1847
|
+
function rollUpProvider(providerKey, findings) {
|
|
1848
|
+
const drifts = findings.filter((f) => f.status === "drift");
|
|
1849
|
+
const present = findings.filter((f) => f.status === "present");
|
|
1850
|
+
const missing = findings.filter((f) => f.status === "missing");
|
|
1851
|
+
const label = providerLabel(providerKey);
|
|
1852
|
+
if (drifts.length > 0) {
|
|
1853
|
+
return {
|
|
1854
|
+
label,
|
|
1855
|
+
icon: chalk.yellow("⚠"),
|
|
1856
|
+
text: chalk.yellow(`${drifts.length} drift${drifts.length === 1 ? "" : "s"} — see above`),
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
if (present.length > 0) {
|
|
1860
|
+
return {
|
|
1861
|
+
label,
|
|
1862
|
+
icon: chalk.green("✓"),
|
|
1863
|
+
text: summarizePresent(providerKey, present, missing),
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
if (missing.length > 0) {
|
|
1867
|
+
// Red ✗ only when the project locally declares this resource
|
|
1868
|
+
// should exist. Otherwise dim · with a softer "no match" label —
|
|
1869
|
+
// a CLI library shouldn't get a red mark for "no Coolify app".
|
|
1870
|
+
const anyExpected = missing.some((f) => f.expected);
|
|
1871
|
+
return {
|
|
1872
|
+
label,
|
|
1873
|
+
icon: anyExpected ? chalk.red("✗") : chalk.dim("·"),
|
|
1874
|
+
text: anyExpected
|
|
1875
|
+
? chalk.red(summarizeMissing(providerKey))
|
|
1876
|
+
: chalk.dim("no match (not declared by this project)"),
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
// Only `info`-level findings → no actionable state to surface.
|
|
1880
|
+
return {
|
|
1881
|
+
label,
|
|
1882
|
+
icon: chalk.dim("·"),
|
|
1883
|
+
text: chalk.dim(findings[0]?.identity ?? ""),
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
/** Compact "what's here" string for a provider that has at least one
|
|
1887
|
+
* present finding. Per-provider tuned to keep the line short. */
|
|
1888
|
+
function summarizePresent(providerKey, present, missing) {
|
|
1889
|
+
const partial = missing.length > 0 ? chalk.dim(` (${missing.length} missing)`) : "";
|
|
1890
|
+
switch (providerKey) {
|
|
1891
|
+
case "coolify": {
|
|
1892
|
+
const apps = present.filter((f) => f.kind === "application").map((f) => f.identity);
|
|
1893
|
+
const projects = present.filter((f) => f.kind === "project");
|
|
1894
|
+
const parts = [];
|
|
1895
|
+
if (apps.length)
|
|
1896
|
+
parts.push(`${apps.length} app: ${apps.join(", ")}`);
|
|
1897
|
+
if (projects.length)
|
|
1898
|
+
parts.push(`${projects.length} project${projects.length === 1 ? "" : "s"}`);
|
|
1899
|
+
return (parts.join(", ") || present[0].identity) + partial;
|
|
1900
|
+
}
|
|
1901
|
+
case "dns": {
|
|
1902
|
+
const zone = present.find((f) => f.kind === "zone");
|
|
1903
|
+
const records = present.filter((f) => f.kind === "dns-record");
|
|
1904
|
+
const base = zone
|
|
1905
|
+
? `${zone.identity} (${records.length} record${records.length === 1 ? "" : "s"})`
|
|
1906
|
+
: `${records.length} record${records.length === 1 ? "" : "s"}`;
|
|
1907
|
+
return base + partial;
|
|
1908
|
+
}
|
|
1909
|
+
case "s3:r2": {
|
|
1910
|
+
const buckets = present.filter((f) => f.kind === "bucket").map((f) => f.identity);
|
|
1911
|
+
return (`${buckets.length} bucket${buckets.length === 1 ? "" : "s"}: ${buckets.join(", ")}` +
|
|
1912
|
+
partial);
|
|
1913
|
+
}
|
|
1914
|
+
case "github": {
|
|
1915
|
+
const repo = present.find((f) => f.kind === "repository");
|
|
1916
|
+
if (repo) {
|
|
1917
|
+
// Detail is "private · default: main · …" — pull just the
|
|
1918
|
+
// visibility (first segment) to keep the line short.
|
|
1919
|
+
const visibility = repo.detail?.split(" · ")[0];
|
|
1920
|
+
return `${repo.identity}${visibility ? chalk.dim(` (${visibility})`) : ""}`;
|
|
1921
|
+
}
|
|
1922
|
+
return present[0].identity;
|
|
1923
|
+
}
|
|
1924
|
+
case "github-pages": {
|
|
1925
|
+
const site = present.find((f) => f.kind === "page-site");
|
|
1926
|
+
if (site) {
|
|
1927
|
+
const cname = site.detail?.match(/cname:\s*([^\s·]+)/)?.[1];
|
|
1928
|
+
return cname ? `live at ${cname}` : "enabled";
|
|
1929
|
+
}
|
|
1930
|
+
return "enabled";
|
|
1931
|
+
}
|
|
1932
|
+
case "resend": {
|
|
1933
|
+
const domains = present.filter((f) => f.kind === "verified-domain").map((f) => f.identity);
|
|
1934
|
+
return domains.join(", ") + partial;
|
|
1935
|
+
}
|
|
1936
|
+
case "glitchtip":
|
|
1937
|
+
case "openpanel": {
|
|
1938
|
+
const projects = present.filter((f) => f.kind === "project").map((f) => f.identity);
|
|
1939
|
+
return projects.join(", ") + partial;
|
|
1940
|
+
}
|
|
1941
|
+
case "stripe": {
|
|
1942
|
+
const hooks = present.filter((f) => f.kind === "webhook-endpoint");
|
|
1943
|
+
return `${hooks.length} webhook${hooks.length === 1 ? "" : "s"}` + partial;
|
|
1944
|
+
}
|
|
1945
|
+
default:
|
|
1946
|
+
return present[0].identity + partial;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
/** Compact "what's not here" string for a provider where every
|
|
1950
|
+
* finding is `missing`. The detailed reason is in the per-provider
|
|
1951
|
+
* block above — this is just the headline. */
|
|
1952
|
+
function summarizeMissing(providerKey) {
|
|
1953
|
+
switch (providerKey) {
|
|
1954
|
+
case "coolify":
|
|
1955
|
+
return "no matching app";
|
|
1956
|
+
case "dns":
|
|
1957
|
+
return "no zone for this domain";
|
|
1958
|
+
case "s3:r2":
|
|
1959
|
+
return "no matching buckets";
|
|
1960
|
+
case "github":
|
|
1961
|
+
return "repo not found";
|
|
1962
|
+
case "github-pages":
|
|
1963
|
+
return "Pages not enabled";
|
|
1964
|
+
case "resend":
|
|
1965
|
+
return "domain not verified";
|
|
1966
|
+
case "glitchtip":
|
|
1967
|
+
case "openpanel":
|
|
1968
|
+
return "no matching project";
|
|
1969
|
+
case "stripe":
|
|
1970
|
+
return "no webhook for this domain";
|
|
1971
|
+
default:
|
|
1972
|
+
return "not found";
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
function providerLabel(key) {
|
|
1976
|
+
switch (key) {
|
|
1977
|
+
case "coolify":
|
|
1978
|
+
return "Coolify";
|
|
1979
|
+
case "dns":
|
|
1980
|
+
return "DNS";
|
|
1981
|
+
case "s3:r2":
|
|
1982
|
+
return "R2";
|
|
1983
|
+
case "s3:hetzner":
|
|
1984
|
+
return "Hetzner S3";
|
|
1985
|
+
case "s3:aws":
|
|
1986
|
+
return "AWS S3";
|
|
1987
|
+
case "github":
|
|
1988
|
+
return "GitHub";
|
|
1989
|
+
case "github-pages":
|
|
1990
|
+
return "Pages";
|
|
1991
|
+
case "resend":
|
|
1992
|
+
return "Resend";
|
|
1993
|
+
case "glitchtip":
|
|
1994
|
+
return "GlitchTip";
|
|
1995
|
+
case "openpanel":
|
|
1996
|
+
return "OpenPanel";
|
|
1997
|
+
case "stripe":
|
|
1998
|
+
return "Stripe";
|
|
1999
|
+
default:
|
|
2000
|
+
return key;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
1522
2003
|
//# sourceMappingURL=inventory.js.map
|