hatchkit 0.1.35 → 0.1.38
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 +167 -1
- package/dist/adopt.js.map +1 -1
- package/dist/assets/env.d.ts +40 -0
- package/dist/assets/env.d.ts.map +1 -0
- package/dist/assets/env.js +150 -0
- package/dist/assets/env.js.map +1 -0
- package/dist/assets/index.d.ts +2 -0
- package/dist/assets/index.d.ts.map +1 -0
- package/dist/assets/index.js +328 -0
- package/dist/assets/index.js.map +1 -0
- package/dist/assets/mirror.d.ts +59 -0
- package/dist/assets/mirror.d.ts.map +1 -0
- package/dist/assets/mirror.js +251 -0
- package/dist/assets/mirror.js.map +1 -0
- package/dist/completion.d.ts.map +1 -1
- package/dist/completion.js +16 -0
- package/dist/completion.js.map +1 -1
- package/dist/config.d.ts +18 -8
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +157 -50
- package/dist/config.js.map +1 -1
- package/dist/deploy/coolify-app.d.ts +6 -0
- package/dist/deploy/coolify-app.d.ts.map +1 -1
- package/dist/deploy/coolify-app.js +58 -15
- package/dist/deploy/coolify-app.js.map +1 -1
- package/dist/deploy/coolify.d.ts.map +1 -1
- package/dist/deploy/coolify.js +50 -25
- package/dist/deploy/coolify.js.map +1 -1
- package/dist/deploy/rename-domain.d.ts.map +1 -1
- package/dist/deploy/rename-domain.js +4 -2
- package/dist/deploy/rename-domain.js.map +1 -1
- package/dist/deploy/rollback.d.ts +11 -0
- package/dist/deploy/rollback.d.ts.map +1 -1
- package/dist/deploy/rollback.js +46 -9
- package/dist/deploy/rollback.js.map +1 -1
- package/dist/deploy/sync.d.ts +98 -0
- package/dist/deploy/sync.d.ts.map +1 -0
- package/dist/deploy/sync.js +354 -0
- package/dist/deploy/sync.js.map +1 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +29 -11
- package/dist/doctor.js.map +1 -1
- package/dist/explain.d.ts.map +1 -1
- package/dist/explain.js +5 -0
- package/dist/explain.js.map +1 -1
- package/dist/index.js +190 -32
- package/dist/index.js.map +1 -1
- package/dist/prompts.d.ts +19 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +81 -20
- package/dist/prompts.js.map +1 -1
- package/dist/provision/s3-buckets.d.ts +12 -5
- package/dist/provision/s3-buckets.d.ts.map +1 -1
- package/dist/provision/s3-buckets.js +14 -7
- package/dist/provision/s3-buckets.js.map +1 -1
- package/dist/provision/stripe.d.ts +85 -16
- package/dist/provision/stripe.d.ts.map +1 -1
- package/dist/provision/stripe.js +334 -28
- package/dist/provision/stripe.js.map +1 -1
- package/dist/provision/write-env.d.ts +7 -0
- package/dist/provision/write-env.d.ts.map +1 -1
- package/dist/provision/write-env.js +18 -0
- package/dist/provision/write-env.js.map +1 -1
- package/dist/scaffold/app.d.ts.map +1 -1
- package/dist/scaffold/app.js +33 -3
- package/dist/scaffold/app.js.map +1 -1
- package/dist/scaffold/infra.js +2 -2
- package/dist/scaffold/infra.js.map +1 -1
- package/dist/scaffold/manifest.d.ts +5 -0
- package/dist/scaffold/manifest.d.ts.map +1 -1
- package/dist/scaffold/manifest.js +1 -0
- package/dist/scaffold/manifest.js.map +1 -1
- package/dist/scaffold/starter-files.d.ts +22 -0
- package/dist/scaffold/starter-files.d.ts.map +1 -1
- package/dist/scaffold/starter-files.js +44 -0
- package/dist/scaffold/starter-files.js.map +1 -1
- package/dist/scaffold/surfaces.d.ts +11 -0
- package/dist/scaffold/surfaces.d.ts.map +1 -0
- package/dist/scaffold/surfaces.js +331 -0
- package/dist/scaffold/surfaces.js.map +1 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +11 -1
- package/dist/status.js.map +1 -1
- package/dist/templates/build-pipeline/docker-compose.yml.hbs +17 -0
- package/dist/utils/cancel-handler.d.ts +17 -0
- package/dist/utils/cancel-handler.d.ts.map +1 -0
- package/dist/utils/cancel-handler.js +97 -0
- package/dist/utils/cancel-handler.js.map +1 -0
- package/dist/utils/coolify-api.d.ts +60 -2
- package/dist/utils/coolify-api.d.ts.map +1 -1
- package/dist/utils/coolify-api.js +85 -2
- package/dist/utils/coolify-api.js.map +1 -1
- package/dist/utils/secrets.d.ts +29 -0
- package/dist/utils/secrets.d.ts.map +1 -1
- package/dist/utils/secrets.js +29 -0
- package/dist/utils/secrets.js.map +1 -1
- package/dist/utils/validate.d.ts +6 -0
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +17 -0
- package/dist/utils/validate.js.map +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type ProjectManifest } from "../scaffold/manifest.js";
|
|
2
|
+
import { type CoolifyApplication } from "../utils/coolify-api.js";
|
|
3
|
+
export interface SyncOptions {
|
|
4
|
+
/** Project root containing `.hatchkit.json`. */
|
|
5
|
+
projectDir: string;
|
|
6
|
+
/** Print the desired changes without PATCHing Coolify. */
|
|
7
|
+
dryRun?: boolean;
|
|
8
|
+
/** Emit `{ ok, apps: [...] }` JSON to stdout. Suppresses the human
|
|
9
|
+
* rendering for scripts. */
|
|
10
|
+
json?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** What sync intends to do for one Coolify application — surfaces both
|
|
13
|
+
* the desired payload and a diff against what Coolify currently reports.
|
|
14
|
+
* Renderable in either human-readable or JSON form. */
|
|
15
|
+
export interface AppSyncPlan {
|
|
16
|
+
/** Coolify uuid. */
|
|
17
|
+
uuid: string;
|
|
18
|
+
/** Coolify app name (used to locate the resource). */
|
|
19
|
+
name: string;
|
|
20
|
+
/** Build pack reported by Coolify — drives which API field carries
|
|
21
|
+
* the domain payload. */
|
|
22
|
+
buildPack?: CoolifyApplication["buildPack"];
|
|
23
|
+
/** Per-service domains for dockercompose apps. Always populated when
|
|
24
|
+
* the build pack is dockercompose; undefined otherwise. */
|
|
25
|
+
desiredDockerComposeDomains?: Array<{
|
|
26
|
+
name: string;
|
|
27
|
+
domain: string;
|
|
28
|
+
}>;
|
|
29
|
+
/** Comma-joined FQDN list for non-dockercompose apps. Always
|
|
30
|
+
* populated when the build pack is nixpacks / dockerfile / static;
|
|
31
|
+
* undefined for dockercompose. */
|
|
32
|
+
desiredDomains?: string[];
|
|
33
|
+
/** ports_exposes the manifest expects on this app. Always set —
|
|
34
|
+
* Coolify keeps it as a non-empty string. */
|
|
35
|
+
desiredPortsExposes: string;
|
|
36
|
+
/** Snapshot of the same fields as Coolify currently reports them.
|
|
37
|
+
* Used by the renderer to decide "already correct" vs. "will
|
|
38
|
+
* change", and by the JSON output as the before-state. */
|
|
39
|
+
current: {
|
|
40
|
+
fqdn: string | null;
|
|
41
|
+
dockerComposeDomains?: Array<{
|
|
42
|
+
name: string;
|
|
43
|
+
domain: string;
|
|
44
|
+
}>;
|
|
45
|
+
portsExposes?: string;
|
|
46
|
+
};
|
|
47
|
+
/** Whether a PATCH is needed to converge — false means everything
|
|
48
|
+
* already matches, sync skips the API call. */
|
|
49
|
+
changed: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface SyncResult {
|
|
52
|
+
ok: boolean;
|
|
53
|
+
/** Set when sync couldn't run at all (e.g. no manifest, no Coolify
|
|
54
|
+
* config, no matching apps). Either `apps` or `error` will be
|
|
55
|
+
* meaningful — never both. */
|
|
56
|
+
error?: string;
|
|
57
|
+
apps: AppSyncPlan[];
|
|
58
|
+
/** When dryRun, no PATCH was made even if `changed` was true. */
|
|
59
|
+
dryRun: boolean;
|
|
60
|
+
}
|
|
61
|
+
/** Top-level entrypoint. Reads the project manifest, finds the Coolify
|
|
62
|
+
* app(s) hatchkit knows about by name, and pushes the desired domain
|
|
63
|
+
* + ports payload — or just prints what it would push when `dryRun`. */
|
|
64
|
+
export declare function runSync(opts: SyncOptions): Promise<SyncResult>;
|
|
65
|
+
/** Desired state for one Coolify application, derived from the manifest.
|
|
66
|
+
* Computed before any API calls so `--dry-run` never hits the network
|
|
67
|
+
* for plan generation. */
|
|
68
|
+
interface DesiredApp {
|
|
69
|
+
/** Name to look up in Coolify. */
|
|
70
|
+
appName: string;
|
|
71
|
+
/** Build-pack-aware payload — only one of these is set per app. The
|
|
72
|
+
* CoolifyApi.updateApplication shape needs the right field for the
|
|
73
|
+
* build pack reported by Coolify; we resolve that at apply time, not
|
|
74
|
+
* here, since the manifest doesn't carry build pack. */
|
|
75
|
+
domains: Array<{
|
|
76
|
+
name: string;
|
|
77
|
+
domain: string;
|
|
78
|
+
}>;
|
|
79
|
+
/** ports_exposes for this app. Comma-separated string Coolify
|
|
80
|
+
* stores verbatim. */
|
|
81
|
+
portsExposes: string;
|
|
82
|
+
}
|
|
83
|
+
/** Map a manifest to the set of Coolify apps hatchkit owns for it. The
|
|
84
|
+
* shapes we cover (matching the layouts `findCoolifyAppsForProject`
|
|
85
|
+
* understands):
|
|
86
|
+
*
|
|
87
|
+
* 1. Adopted single-app: `<name>`
|
|
88
|
+
* 2. Starter split (legacy): `<name>-server` + `<name>-client`
|
|
89
|
+
* 3. Old single-app fallbacks: `<name>-web` / `<name>-app` / `<name>-api`
|
|
90
|
+
*
|
|
91
|
+
* We synthesize a candidate list per layout. The actual lookup happens
|
|
92
|
+
* per-name; misses are skipped. This means a project that scaffolded as
|
|
93
|
+
* starter-split AND was later adopted as single-app would push twice —
|
|
94
|
+
* not a problem because each PATCH is independent and idempotent. */
|
|
95
|
+
export declare function computeDesiredAppStates(manifest: ProjectManifest): DesiredApp[];
|
|
96
|
+
export declare function runSyncCli(args: string[]): Promise<void>;
|
|
97
|
+
export {};
|
|
98
|
+
//# sourceMappingURL=sync.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/deploy/sync.ts"],"names":[],"mappings":"AAoCA,OAAO,EAAE,KAAK,eAAe,EAAgB,MAAM,yBAAyB,CAAC;AAC7E,OAAO,EAAc,KAAK,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE9E,MAAM,WAAW,WAAW;IAC1B,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;iCAC6B;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;wDAEwD;AACxD,MAAM,WAAW,WAAW;IAC1B,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb;8BAC0B;IAC1B,SAAS,CAAC,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;IAC5C;gEAC4D;IAC5D,2BAA2B,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtE;;uCAEmC;IACnC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;kDAC8C;IAC9C,mBAAmB,EAAE,MAAM,CAAC;IAC5B;;+DAE2D;IAC3D,OAAO,EAAE;QACP,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;QACpB,oBAAoB,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;QAC/D,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IACF;oDACgD;IAChD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ;;mCAE+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,WAAW,EAAE,CAAC;IACpB,iEAAiE;IACjE,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;yEAEyE;AACzE,wBAAsB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAsHpE;AAMD;;2BAE2B;AAC3B,UAAU,UAAU;IAClB,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB;;;6DAGyD;IACzD,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACjD;2BACuB;IACvB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;sEAWsE;AACtE,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,eAAe,GAAG,UAAU,EAAE,CA6E/E;AA2HD,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAe9D"}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* sync — push the .hatchkit.json manifest's view of the project onto
|
|
3
|
+
* the Coolify resource(s) hatchkit created (or adopted) for it.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: Coolify's auto-generated Traefik labels are derived
|
|
6
|
+
* from the application's Domain field (`docker_compose_domains` for
|
|
7
|
+
* dockercompose build packs, `fqdn` / `domains` otherwise). When a
|
|
8
|
+
* scaffold or adopt run created the app without that field populated —
|
|
9
|
+
* either because of the pre-fix bug where `updateApplication` couldn't
|
|
10
|
+
* push domains, or because the user changed the manifest after scaffold —
|
|
11
|
+
* the container ends up with zero traefik labels and Traefik silently
|
|
12
|
+
* drops the route. `hatchkit sync` reads the manifest, finds the matching
|
|
13
|
+
* Coolify app(s), and PATCHes them so Coolify regenerates the labels on
|
|
14
|
+
* the next deploy.
|
|
15
|
+
*
|
|
16
|
+
* Scope is deliberately narrow. Sync only pushes fields that are safe to
|
|
17
|
+
* blast over the wire idempotently:
|
|
18
|
+
* · domain (`docker_compose_domains` for compose apps; `domains` for
|
|
19
|
+
* nixpacks / dockerfile / static)
|
|
20
|
+
* · ports_exposes (so the multi-host routing for the starter's split
|
|
21
|
+
* compose stays consistent with what runCoolifySetup creates)
|
|
22
|
+
*
|
|
23
|
+
* Out of scope (handled by other commands):
|
|
24
|
+
* · env vars → `hatchkit keys push` + adopt's setAppEnv
|
|
25
|
+
* · DNS records → adopt's wireDns + `rename-domain`
|
|
26
|
+
* · ML services / GPU → `hatchkit add gpu`
|
|
27
|
+
* · S3 buckets / tokens → `hatchkit provision s3`
|
|
28
|
+
*
|
|
29
|
+
* Idempotent by design: reads current state first, only PATCHes when the
|
|
30
|
+
* desired domain set differs from what Coolify reports. `--dry-run`
|
|
31
|
+
* shows the diff without touching anything.
|
|
32
|
+
*/
|
|
33
|
+
import chalk from "chalk";
|
|
34
|
+
import ora from "ora";
|
|
35
|
+
import { getCoolifyConfig } from "../config.js";
|
|
36
|
+
import { readManifest } from "../scaffold/manifest.js";
|
|
37
|
+
import { CoolifyApi } from "../utils/coolify-api.js";
|
|
38
|
+
/** Top-level entrypoint. Reads the project manifest, finds the Coolify
|
|
39
|
+
* app(s) hatchkit knows about by name, and pushes the desired domain
|
|
40
|
+
* + ports payload — or just prints what it would push when `dryRun`. */
|
|
41
|
+
export async function runSync(opts) {
|
|
42
|
+
const manifest = readManifest(opts.projectDir);
|
|
43
|
+
if (!manifest) {
|
|
44
|
+
const err = `No .hatchkit.json found in ${opts.projectDir}.`;
|
|
45
|
+
if (!opts.json) {
|
|
46
|
+
console.log(chalk.red(` ${err}`));
|
|
47
|
+
console.log(chalk.dim(" Run `hatchkit sync` from a hatchkit-scaffolded project root, or `hatchkit adopt` to onboard an existing project first."));
|
|
48
|
+
}
|
|
49
|
+
return { ok: false, error: err, apps: [], dryRun: !!opts.dryRun };
|
|
50
|
+
}
|
|
51
|
+
const cfg = await getCoolifyConfig();
|
|
52
|
+
if (!cfg) {
|
|
53
|
+
const err = "Coolify is not configured. Run `hatchkit config add coolify` first.";
|
|
54
|
+
if (!opts.json)
|
|
55
|
+
console.log(chalk.red(` ${err}`));
|
|
56
|
+
return { ok: false, error: err, apps: [], dryRun: !!opts.dryRun };
|
|
57
|
+
}
|
|
58
|
+
const api = new CoolifyApi({ url: cfg.url, token: cfg.token });
|
|
59
|
+
const desiredAll = computeDesiredAppStates(manifest);
|
|
60
|
+
const apps = [];
|
|
61
|
+
const errors = [];
|
|
62
|
+
// Match every desired-app entry against Coolify by name. We don't
|
|
63
|
+
// pre-list /applications and intersect because sync should still work
|
|
64
|
+
// when the user has hundreds of apps; a per-name lookup is cheaper.
|
|
65
|
+
// Apps the manifest expects but that don't exist in Coolify are
|
|
66
|
+
// logged as a hint (the user probably needs `hatchkit adopt` first)
|
|
67
|
+
// but don't fail the whole run — partial sync of the apps that DO
|
|
68
|
+
// exist is the most useful behavior.
|
|
69
|
+
for (const desired of desiredAll) {
|
|
70
|
+
const matchSpinner = opts.json ? null : ora(`Coolify: locating "${desired.appName}"`).start();
|
|
71
|
+
const found = await api.findApplicationByName(desired.appName);
|
|
72
|
+
if (!found) {
|
|
73
|
+
matchSpinner?.warn(`Coolify: no app named "${desired.appName}" — skipping`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
matchSpinner?.succeed(`Coolify: found "${desired.appName}" (${found.uuid})`);
|
|
77
|
+
let current;
|
|
78
|
+
try {
|
|
79
|
+
current = await api.getApplication(found.uuid);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
errors.push(`Failed to read Coolify app "${desired.appName}" (${found.uuid}): ${err.message}`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const plan = buildPlan(found.uuid, desired, current);
|
|
86
|
+
apps.push(plan);
|
|
87
|
+
if (!opts.json)
|
|
88
|
+
renderPlan(plan);
|
|
89
|
+
if (!plan.changed)
|
|
90
|
+
continue;
|
|
91
|
+
if (opts.dryRun)
|
|
92
|
+
continue;
|
|
93
|
+
const patch = ora(`Coolify: updating "${desired.appName}"`).start();
|
|
94
|
+
try {
|
|
95
|
+
await api.updateApplication(plan.uuid, {
|
|
96
|
+
portsExposes: plan.desiredPortsExposes,
|
|
97
|
+
...(plan.desiredDockerComposeDomains
|
|
98
|
+
? { dockerComposeDomains: plan.desiredDockerComposeDomains }
|
|
99
|
+
: {}),
|
|
100
|
+
...(plan.desiredDomains ? { domains: plan.desiredDomains } : {}),
|
|
101
|
+
});
|
|
102
|
+
patch.succeed(`Coolify: updated "${desired.appName}"`);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
patch.fail(`Coolify: PATCH failed: ${err.message}`);
|
|
106
|
+
errors.push(`PATCH ${desired.appName}: ${err.message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (apps.length === 0 && errors.length === 0) {
|
|
110
|
+
const err = `No Coolify apps matched manifest project "${manifest.name}".`;
|
|
111
|
+
if (!opts.json) {
|
|
112
|
+
console.log(chalk.yellow(` ${err}`));
|
|
113
|
+
console.log(chalk.dim(` Looked for: ${desiredAll.map((d) => `"${d.appName}"`).join(", ")}.\n` +
|
|
114
|
+
` Run \`hatchkit adopt\` to create them, or rename the existing app(s) to match.`));
|
|
115
|
+
}
|
|
116
|
+
return { ok: false, error: err, apps, dryRun: !!opts.dryRun };
|
|
117
|
+
}
|
|
118
|
+
if (!opts.json) {
|
|
119
|
+
if (opts.dryRun) {
|
|
120
|
+
console.log(chalk.dim("\n --dry-run: no changes pushed."));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const changed = apps.filter((a) => a.changed);
|
|
124
|
+
if (changed.length === 0) {
|
|
125
|
+
console.log(chalk.green("\n ✓ Coolify already in sync with manifest."));
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.log(chalk.green(`\n ✓ Synced ${changed.length} app(s) to manifest state.`) +
|
|
129
|
+
chalk.dim("\n Trigger a redeploy in Coolify (or push a commit) for Traefik to pick up the new labels."));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (errors.length > 0) {
|
|
133
|
+
console.log(chalk.yellow("\n Errors:"));
|
|
134
|
+
for (const e of errors)
|
|
135
|
+
console.log(chalk.yellow(` · ${e}`));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
ok: errors.length === 0,
|
|
140
|
+
apps,
|
|
141
|
+
dryRun: !!opts.dryRun,
|
|
142
|
+
...(errors.length > 0 ? { error: errors.join("; ") } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** Map a manifest to the set of Coolify apps hatchkit owns for it. The
|
|
146
|
+
* shapes we cover (matching the layouts `findCoolifyAppsForProject`
|
|
147
|
+
* understands):
|
|
148
|
+
*
|
|
149
|
+
* 1. Adopted single-app: `<name>`
|
|
150
|
+
* 2. Starter split (legacy): `<name>-server` + `<name>-client`
|
|
151
|
+
* 3. Old single-app fallbacks: `<name>-web` / `<name>-app` / `<name>-api`
|
|
152
|
+
*
|
|
153
|
+
* We synthesize a candidate list per layout. The actual lookup happens
|
|
154
|
+
* per-name; misses are skipped. This means a project that scaffolded as
|
|
155
|
+
* starter-split AND was later adopted as single-app would push twice —
|
|
156
|
+
* not a problem because each PATCH is independent and idempotent. */
|
|
157
|
+
export function computeDesiredAppStates(manifest) {
|
|
158
|
+
const { name, domain, surfaces, ports } = manifest;
|
|
159
|
+
const portServer = String(ports?.server ?? 3000);
|
|
160
|
+
const portClient = String(ports?.client ?? 3001);
|
|
161
|
+
// Routing recipes — see `runCoolifySetup` (cli/src/deploy/coolify.ts)
|
|
162
|
+
// for the create-time source of truth. Sync mirrors that exactly so
|
|
163
|
+
// re-running sync converges to the same labels Coolify generated at
|
|
164
|
+
// create time.
|
|
165
|
+
const apiDomain = `api.${domain}`;
|
|
166
|
+
const frontendDomain = `https://${domain}`;
|
|
167
|
+
const backendDomains = [
|
|
168
|
+
`https://${apiDomain}`,
|
|
169
|
+
`https://${domain}/api`,
|
|
170
|
+
`https://${domain}/api/ws`,
|
|
171
|
+
`https://${apiDomain}/ws`,
|
|
172
|
+
];
|
|
173
|
+
const splitClientDomains = [{ name: "client", domain: frontendDomain }];
|
|
174
|
+
const splitServerDomains = backendDomains.map((d) => ({ name: "server", domain: d }));
|
|
175
|
+
// Single-app layout: one Coolify app named `<name>` with one compose
|
|
176
|
+
// service `app`. ports_exposes is surface-aware:
|
|
177
|
+
// server-only / both → server port (the public listener)
|
|
178
|
+
// client-only → 80 (matches adopt.ts's static-site default)
|
|
179
|
+
const singleAppPort = surfaces === "client-only" ? "80" : portServer;
|
|
180
|
+
const singleAppDomain = surfaces === "client-only" && (singleAppPort === "80" || singleAppPort === "443")
|
|
181
|
+
? `https://${domain}`
|
|
182
|
+
: `https://${domain}:${singleAppPort}`;
|
|
183
|
+
// Use bare `https://<domain>` when the listener is on the conventional
|
|
184
|
+
// 80/443 — Coolify's Traefik handles the HTTPS termination and the
|
|
185
|
+
// explicit port suffix would push the route through Traefik on a
|
|
186
|
+
// non-standard port (which won't match the Coolify ingress). The
|
|
187
|
+
// formatDockerComposeDomain helper in coolify-app.ts uses the same
|
|
188
|
+
// rule; mirror it here so sync output matches what adopt creates.
|
|
189
|
+
const singleAppCanonicalDomain = singleAppPort === "80" || singleAppPort === "443" ? `https://${domain}` : singleAppDomain;
|
|
190
|
+
const singleApp = {
|
|
191
|
+
appName: name,
|
|
192
|
+
domains: [{ name: "app", domain: singleAppCanonicalDomain }],
|
|
193
|
+
portsExposes: singleAppPort,
|
|
194
|
+
};
|
|
195
|
+
// Starter-split layout: two apps. Each app's compose has its own
|
|
196
|
+
// service named `client` or `server` respectively; routing splits
|
|
197
|
+
// along the same lines as runCoolifySetup creates.
|
|
198
|
+
const splitClient = {
|
|
199
|
+
appName: `${name}-client`,
|
|
200
|
+
domains: splitClientDomains,
|
|
201
|
+
portsExposes: portClient,
|
|
202
|
+
};
|
|
203
|
+
const splitServer = {
|
|
204
|
+
appName: `${name}-server`,
|
|
205
|
+
domains: splitServerDomains,
|
|
206
|
+
portsExposes: portServer,
|
|
207
|
+
};
|
|
208
|
+
// Old single-app fallbacks. `runCoolifySetup` creates `<name>-web`
|
|
209
|
+
// for the very-old starter shape; the others are speculative for
|
|
210
|
+
// hand-written compose layouts that adopt previously matched.
|
|
211
|
+
const fallbackWeb = {
|
|
212
|
+
...singleApp,
|
|
213
|
+
appName: `${name}-web`,
|
|
214
|
+
};
|
|
215
|
+
// Filter by surfaces so we don't ship a non-existent split shape
|
|
216
|
+
// in JSON output. The actual Coolify lookup will skip non-existent
|
|
217
|
+
// names anyway, but keeping the candidate list tight reduces noise.
|
|
218
|
+
if (surfaces === "client-only") {
|
|
219
|
+
return [singleApp, fallbackWeb, splitClient];
|
|
220
|
+
}
|
|
221
|
+
if (surfaces === "server-only") {
|
|
222
|
+
return [singleApp, fallbackWeb, splitServer];
|
|
223
|
+
}
|
|
224
|
+
// both / undefined → server-of-truth is the split layout, but adopt
|
|
225
|
+
// collapses to single-app for projects without a separate frontend.
|
|
226
|
+
return [singleApp, fallbackWeb, splitServer, splitClient];
|
|
227
|
+
}
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Plan rendering
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
function buildPlan(uuid, desired, current) {
|
|
232
|
+
const isCompose = current.buildPack === "dockercompose";
|
|
233
|
+
// dockercompose apps use docker_compose_domains; everything else uses
|
|
234
|
+
// the flat `domains` field. Coolify rejects a domain payload that
|
|
235
|
+
// doesn't match the build pack with a 422.
|
|
236
|
+
const desiredDockerComposeDomains = isCompose ? desired.domains : undefined;
|
|
237
|
+
const desiredDomains = isCompose ? undefined : desired.domains.map((d) => d.domain);
|
|
238
|
+
const portsChanged = current.portsExposes !== undefined && current.portsExposes !== desired.portsExposes;
|
|
239
|
+
const domainsChanged = isCompose
|
|
240
|
+
? !sameDockerComposeDomains(current.dockerComposeDomains, desired.domains)
|
|
241
|
+
: !sameStringList(splitFqdn(current.fqdn), desired.domains.map((d) => d.domain));
|
|
242
|
+
return {
|
|
243
|
+
uuid,
|
|
244
|
+
name: current.name || desired.appName,
|
|
245
|
+
buildPack: current.buildPack,
|
|
246
|
+
...(desiredDockerComposeDomains ? { desiredDockerComposeDomains } : {}),
|
|
247
|
+
...(desiredDomains ? { desiredDomains } : {}),
|
|
248
|
+
desiredPortsExposes: desired.portsExposes,
|
|
249
|
+
current: {
|
|
250
|
+
fqdn: current.fqdn,
|
|
251
|
+
...(current.dockerComposeDomains
|
|
252
|
+
? { dockerComposeDomains: current.dockerComposeDomains }
|
|
253
|
+
: {}),
|
|
254
|
+
...(current.portsExposes !== undefined ? { portsExposes: current.portsExposes } : {}),
|
|
255
|
+
},
|
|
256
|
+
changed: portsChanged || domainsChanged,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function renderPlan(plan) {
|
|
260
|
+
console.log(chalk.bold(`\n ${plan.name}`) + chalk.dim(` (${plan.uuid.slice(0, 8)}…)`));
|
|
261
|
+
if (plan.buildPack) {
|
|
262
|
+
console.log(chalk.dim(` build pack: ${plan.buildPack}`));
|
|
263
|
+
}
|
|
264
|
+
if (plan.desiredDockerComposeDomains) {
|
|
265
|
+
const before = plan.current.dockerComposeDomains ?? [];
|
|
266
|
+
const after = plan.desiredDockerComposeDomains;
|
|
267
|
+
const same = sameDockerComposeDomains(before, after);
|
|
268
|
+
if (same) {
|
|
269
|
+
console.log(chalk.green(` ✓ docker_compose_domains: in sync`));
|
|
270
|
+
console.log(chalk.dim(` ${formatDockerComposeDomains(after)}`));
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
console.log(chalk.yellow(` · docker_compose_domains:`));
|
|
274
|
+
console.log(chalk.dim(` before: ${formatDockerComposeDomains(before)}`));
|
|
275
|
+
console.log(chalk.dim(` after: ${formatDockerComposeDomains(after)}`));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else if (plan.desiredDomains) {
|
|
279
|
+
const before = splitFqdn(plan.current.fqdn);
|
|
280
|
+
const after = plan.desiredDomains;
|
|
281
|
+
const same = sameStringList(before, after);
|
|
282
|
+
if (same) {
|
|
283
|
+
console.log(chalk.green(` ✓ domains: in sync (${after.join(", ")})`));
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
console.log(chalk.yellow(` · domains:`));
|
|
287
|
+
console.log(chalk.dim(` before: ${before.join(", ") || "(empty)"}`));
|
|
288
|
+
console.log(chalk.dim(` after: ${after.join(", ")}`));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (plan.current.portsExposes !== undefined &&
|
|
292
|
+
plan.current.portsExposes !== plan.desiredPortsExposes) {
|
|
293
|
+
console.log(chalk.yellow(` · ports_exposes:`));
|
|
294
|
+
console.log(chalk.dim(` before: ${plan.current.portsExposes}`));
|
|
295
|
+
console.log(chalk.dim(` after: ${plan.desiredPortsExposes}`));
|
|
296
|
+
}
|
|
297
|
+
else if (plan.current.portsExposes === plan.desiredPortsExposes) {
|
|
298
|
+
console.log(chalk.green(` ✓ ports_exposes: ${plan.desiredPortsExposes}`));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Helpers
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
function splitFqdn(fqdn) {
|
|
305
|
+
if (!fqdn)
|
|
306
|
+
return [];
|
|
307
|
+
return fqdn
|
|
308
|
+
.split(",")
|
|
309
|
+
.map((s) => s.trim())
|
|
310
|
+
.filter(Boolean);
|
|
311
|
+
}
|
|
312
|
+
function sameStringList(a, b) {
|
|
313
|
+
if (a.length !== b.length)
|
|
314
|
+
return false;
|
|
315
|
+
const sa = [...a].sort();
|
|
316
|
+
const sb = [...b].sort();
|
|
317
|
+
return sa.every((v, i) => v === sb[i]);
|
|
318
|
+
}
|
|
319
|
+
function sameDockerComposeDomains(a, b) {
|
|
320
|
+
const left = a ?? [];
|
|
321
|
+
if (left.length !== b.length)
|
|
322
|
+
return false;
|
|
323
|
+
// Order-insensitive comparison — Coolify doesn't promise to round-trip
|
|
324
|
+
// the array in the same order it was sent.
|
|
325
|
+
const key = (e) => `${e.name}::${e.domain}`;
|
|
326
|
+
const setA = new Set(left.map(key));
|
|
327
|
+
return b.every((e) => setA.has(key(e)));
|
|
328
|
+
}
|
|
329
|
+
function formatDockerComposeDomains(entries) {
|
|
330
|
+
if (entries.length === 0)
|
|
331
|
+
return "(empty)";
|
|
332
|
+
return entries.map((e) => `${e.name}=${e.domain}`).join(", ");
|
|
333
|
+
}
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
// CLI glue — thin wrapper the dispatcher calls.
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
export async function runSyncCli(args) {
|
|
338
|
+
const dryRun = args.includes("--dry-run");
|
|
339
|
+
const json = args.includes("--json");
|
|
340
|
+
const dirArg = (() => {
|
|
341
|
+
const i = args.findIndex((a) => a === "--dir");
|
|
342
|
+
if (i >= 0 && args[i + 1])
|
|
343
|
+
return args[i + 1];
|
|
344
|
+
return undefined;
|
|
345
|
+
})();
|
|
346
|
+
const projectDir = dirArg ? dirArg : process.cwd();
|
|
347
|
+
const result = await runSync({ projectDir, dryRun, json });
|
|
348
|
+
if (json) {
|
|
349
|
+
console.log(JSON.stringify(result, null, 2));
|
|
350
|
+
}
|
|
351
|
+
if (!result.ok)
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
//# sourceMappingURL=sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../../src/deploy/sync.ts"],"names":[],"mappings":"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"}
|
package/dist/doctor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAuBA,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAuBA,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAugBD,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAoBnE;AAED;;;;;;;;;;;0CAW0C;AAC1C,wBAAsB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CA6FrF;AAED;;;;;;;;;;;;;+CAa+C;AAC/C,wBAAsB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CA6HxF;AAED,wBAAsB,SAAS,CAAC,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CA8C5E"}
|
package/dist/doctor.js
CHANGED
|
@@ -423,30 +423,47 @@ async function checkResend() {
|
|
|
423
423
|
return undefined;
|
|
424
424
|
});
|
|
425
425
|
}
|
|
426
|
-
async function
|
|
427
|
-
|
|
428
|
-
const cfg = await getStripeConfig();
|
|
429
|
-
if (!cfg)
|
|
430
|
-
return { name: "Stripe", status: "skip" };
|
|
431
|
-
return check(`Stripe (${cfg.mode})`, async () => {
|
|
426
|
+
async function checkStripeMode(mode, secretKey) {
|
|
427
|
+
return check(`Stripe (${mode} master)`, async () => {
|
|
432
428
|
const res = await fetch("https://api.stripe.com/v1/balance", {
|
|
433
|
-
headers: { Authorization: `Bearer ${
|
|
429
|
+
headers: { Authorization: `Bearer ${secretKey}` },
|
|
434
430
|
});
|
|
435
431
|
if (!res.ok)
|
|
436
432
|
throw new Error(`HTTP ${res.status}`);
|
|
437
|
-
|
|
433
|
+
// The webhook_endpoints:write scope can't be cheaply tested with
|
|
434
|
+
// a GET, so /balance is the proxy: it proves the key is live and
|
|
435
|
+
// the account is reachable. A scope-mismatched key still passes
|
|
436
|
+
// /balance — at provision time, POST /v1/webhook_endpoints will
|
|
437
|
+
// surface the scope error inline (and `hatchkit create` already
|
|
438
|
+
// soft-fails to a manual fallback when that happens).
|
|
439
|
+
return "master key valid";
|
|
438
440
|
}, (detail) => {
|
|
439
441
|
const code = httpCode(detail);
|
|
440
442
|
if (code === 401) {
|
|
441
443
|
return [
|
|
442
|
-
|
|
443
|
-
|
|
444
|
+
`Stripe ${mode} master key is invalid or was rotated.`,
|
|
445
|
+
`Create a new restricted key (${mode} mode) at https://dashboard.stripe.com/apikeys`,
|
|
446
|
+
"Required scope: Webhook Endpoints — Write",
|
|
444
447
|
"Then re-run: `hatchkit config add stripe`",
|
|
445
448
|
];
|
|
446
449
|
}
|
|
447
450
|
return undefined;
|
|
448
451
|
});
|
|
449
452
|
}
|
|
453
|
+
async function checkStripe() {
|
|
454
|
+
const { getStripeConfig } = await import("./config.js");
|
|
455
|
+
const cfg = await getStripeConfig();
|
|
456
|
+
if (!cfg)
|
|
457
|
+
return [{ name: "Stripe", status: "skip" }];
|
|
458
|
+
const out = [];
|
|
459
|
+
if (cfg.testSecretKey)
|
|
460
|
+
out.push(await checkStripeMode("test", cfg.testSecretKey));
|
|
461
|
+
if (cfg.liveSecretKey)
|
|
462
|
+
out.push(await checkStripeMode("live", cfg.liveSecretKey));
|
|
463
|
+
if (out.length === 0)
|
|
464
|
+
return [{ name: "Stripe", status: "skip" }];
|
|
465
|
+
return out;
|
|
466
|
+
}
|
|
450
467
|
export async function collectDoctorResults() {
|
|
451
468
|
const results = [];
|
|
452
469
|
results.push(await checkGitHub());
|
|
@@ -460,7 +477,8 @@ export async function collectDoctorResults() {
|
|
|
460
477
|
results.push(await checkGlitchtip());
|
|
461
478
|
results.push(await checkOpenpanel());
|
|
462
479
|
results.push(await checkResend());
|
|
463
|
-
|
|
480
|
+
for (const r of await checkStripe())
|
|
481
|
+
results.push(r);
|
|
464
482
|
// Project-local checks — only run when doctor was invoked inside a
|
|
465
483
|
// hatchkit-managed project (manifest at cwd). Globally they're a
|
|
466
484
|
// no-op, so `hatchkit doctor` from $HOME stays clean.
|