@celilo/cli 0.3.30 → 0.4.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/drizzle/0005_module_operations.sql +12 -0
- package/drizzle/0006_base_module_aspects.sql +15 -0
- package/drizzle/0007_module_systems.sql +17 -0
- package/drizzle/meta/_journal.json +21 -0
- package/package.json +5 -4
- package/schemas/system_config.json +14 -28
- package/src/ansible/inventory.test.ts +46 -62
- package/src/ansible/inventory.ts +48 -25
- package/src/capabilities/registration.ts +25 -7
- package/src/capabilities/validation.test.ts +30 -0
- package/src/capabilities/validation.ts +8 -0
- package/src/cli/backup-rename.test.ts +95 -0
- package/src/cli/cli.test.ts +17 -23
- package/src/cli/command-registry.ts +199 -0
- package/src/cli/commands/backup-list.ts +1 -1
- package/src/cli/commands/events.ts +96 -0
- package/src/cli/commands/machine-add.ts +103 -59
- package/src/cli/commands/module-import.ts +153 -4
- package/src/cli/commands/module-remove.ts +86 -17
- package/src/cli/commands/module-status.ts +6 -2
- package/src/cli/commands/publish/alpha.test.ts +185 -0
- package/src/cli/commands/publish/alpha.ts +226 -0
- package/src/cli/commands/publish/changesets.test.ts +89 -0
- package/src/cli/commands/publish/changesets.ts +144 -0
- package/src/cli/commands/publish/consumer-pins.test.ts +155 -0
- package/src/cli/commands/publish/consumer-pins.ts +149 -0
- package/src/cli/commands/publish/execute.ts +131 -0
- package/src/cli/commands/publish/global-install.test.ts +154 -0
- package/src/cli/commands/publish/global-install.ts +171 -0
- package/src/cli/commands/publish/helpers.ts +227 -0
- package/src/cli/commands/publish/index.ts +365 -0
- package/src/cli/commands/publish/module-registry.test.ts +40 -0
- package/src/cli/commands/publish/module-registry.ts +64 -0
- package/src/cli/commands/publish/plan.ts +107 -0
- package/src/cli/commands/publish/preflight.ts +238 -0
- package/src/cli/commands/publish/types.ts +264 -0
- package/src/cli/commands/publish/workspace.test.ts +323 -0
- package/src/cli/commands/publish/workspace.ts +596 -0
- package/src/cli/commands/restore.ts +126 -0
- package/src/cli/commands/storage-add-local.ts +1 -1
- package/src/cli/commands/storage-add-s3.ts +1 -1
- package/src/cli/commands/subscribers-add.ts +68 -0
- package/src/cli/commands/subscribers-list.ts +48 -0
- package/src/cli/commands/subscribers-remove.ts +38 -0
- package/src/cli/commands/subscribers-serve.ts +77 -0
- package/src/cli/commands/subscribers-status.ts +33 -0
- package/src/cli/commands/subscribers-test.ts +71 -0
- package/src/cli/commands/system-apply-config-equivalence.test.ts +108 -0
- package/src/cli/commands/system-apply-config.test.ts +70 -0
- package/src/cli/commands/system-apply-config.ts +130 -0
- package/src/cli/commands/system-audit.ts +2 -1
- package/src/cli/commands/system-init-deprecation.test.ts +90 -0
- package/src/cli/commands/system-init.ts +36 -70
- package/src/cli/commands/system-update.ts +3 -2
- package/src/cli/completion.ts +22 -1
- package/src/cli/index.ts +214 -6
- package/src/cli/interactive-config.test.ts +19 -0
- package/src/cli/restore-command.test.ts +131 -0
- package/src/db/client.ts +42 -0
- package/src/db/schema.test.ts +13 -16
- package/src/db/schema.ts +161 -9
- package/src/hooks/capability-loader-firewall.test.ts +6 -15
- package/src/hooks/capability-loader.test.ts +2 -3
- package/src/hooks/capability-loader.ts +36 -2
- package/src/hooks/define-hook.test.ts +4 -0
- package/src/hooks/executor.test.ts +18 -0
- package/src/hooks/executor.ts +21 -2
- package/src/hooks/load-hook-config.test.ts +26 -24
- package/src/hooks/load-hook-config.ts +11 -2
- package/src/hooks/run-named-hook.ts +16 -0
- package/src/hooks/types.ts +9 -1
- package/src/manifest/contracts/v1.ts +70 -0
- package/src/manifest/schema.ts +262 -16
- package/src/manifest/validate-privileged.test.ts +84 -0
- package/src/manifest/validate.test.ts +156 -0
- package/src/manifest/validate.ts +69 -0
- package/src/module/import.ts +12 -0
- package/src/services/aspect-approvals.test.ts +231 -0
- package/src/services/aspect-approvals.ts +120 -0
- package/src/services/aspect-runner.test.ts +493 -0
- package/src/services/aspect-runner.ts +438 -0
- package/src/services/aspect-template-resolver.test.ts +101 -0
- package/src/services/aspect-template-resolver.ts +122 -0
- package/src/services/backup-create.ts +104 -25
- package/src/services/backup-envelope-roundtrip.test.ts +199 -0
- package/src/services/backup-in-flight-refusal.test.ts +163 -0
- package/src/services/backup-manifest.test.ts +115 -0
- package/src/services/backup-manifest.ts +163 -0
- package/src/services/backup-restore.ts +154 -19
- package/src/services/build-bus/delivery-events.ts +92 -0
- package/src/services/build-bus/event-factory.ts +54 -0
- package/src/services/build-bus/fan-out.test.ts +279 -0
- package/src/services/build-bus/fan-out.ts +161 -0
- package/src/services/build-bus/hook-dispatch-mgmt.test.ts +157 -0
- package/src/services/build-bus/hook-dispatch.test.ts +207 -0
- package/src/services/build-bus/hook-dispatch.ts +198 -0
- package/src/services/build-bus/hook-dispatcher.ts +115 -0
- package/src/services/build-bus/index.ts +41 -0
- package/src/services/build-bus/receiver-server.test.ts +179 -0
- package/src/services/build-bus/receiver-server.ts +159 -0
- package/src/services/build-bus/status.test.ts +212 -0
- package/src/services/build-bus/status.ts +213 -0
- package/src/services/build-bus/subscriber-store.ts +113 -0
- package/src/services/celilo-events.test.ts +70 -0
- package/src/services/celilo-events.ts +92 -0
- package/src/services/celilo-mgmt-hooks.test.ts +296 -0
- package/src/services/config-interview.ts +13 -95
- package/src/services/cross-module-data-manager.ts +2 -31
- package/src/services/cross-module-read.test.ts +250 -0
- package/src/services/cross-module-read.ts +232 -0
- package/src/services/deploy-validation.ts +7 -0
- package/src/services/deployed-systems.test.ts +235 -0
- package/src/services/deployed-systems.ts +308 -0
- package/src/services/dns-provider-backfill.ts +75 -0
- package/src/services/health-runner.ts +19 -3
- package/src/services/infrastructure-variable-resolver.test.ts +6 -32
- package/src/services/infrastructure-variable-resolver.ts +3 -13
- package/src/services/machine-detector.ts +104 -48
- package/src/services/machine-pool.ts +145 -2
- package/src/services/module-config.ts +78 -120
- package/src/services/module-deploy.ts +113 -40
- package/src/services/module-operations.test.ts +154 -0
- package/src/services/module-operations.ts +154 -0
- package/src/services/module-subscriptions.test.ts +58 -0
- package/src/services/module-subscriptions.ts +24 -1
- package/src/services/module-types-generator.test.ts +3 -3
- package/src/services/module-types-generator.ts +7 -2
- package/src/services/proxmox-reconcile.test.ts +333 -0
- package/src/services/proxmox-reconcile.ts +156 -0
- package/src/services/proxmox-state-recovery.ts +3 -24
- package/src/services/restore-from-file.test.ts +177 -0
- package/src/services/restore-from-file.ts +355 -0
- package/src/services/restore-preflight.test.ts +127 -0
- package/src/services/restore-preflight.ts +118 -0
- package/src/services/storage-providers/s3.ts +10 -2
- package/src/services/system-identity.ts +30 -0
- package/src/services/system-init.test.ts +64 -21
- package/src/services/system-init.ts +28 -26
- package/src/templates/generator.test.ts +7 -16
- package/src/templates/generator.ts +28 -115
- package/src/test-utils/integration.ts +5 -2
- package/src/types/infrastructure.ts +8 -0
- package/src/variables/computed/computed-integration.test.ts +191 -0
- package/src/variables/computed/computed.test.ts +177 -0
- package/src/variables/computed/evaluate.ts +271 -0
- package/src/variables/computed/marker.ts +53 -0
- package/src/variables/computed/parse.ts +262 -0
- package/src/variables/computed/provider-lookup.ts +130 -0
- package/src/variables/context.test.ts +89 -28
- package/src/variables/context.ts +196 -191
- package/src/variables/parser.ts +3 -3
- package/src/variables/resolver.test.ts +61 -0
- package/src/variables/resolver.ts +81 -0
- package/src/variables/types.ts +23 -1
- package/src/services/dns-auto-register.ts +0 -211
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `celilo publish` entry point — thin orchestration over the
|
|
3
|
+
* planner/executor split (v2/PUBLILO_CLI.md Phase 2).
|
|
4
|
+
*
|
|
5
|
+
* parse args → preflight → build plan → display → confirm → execute
|
|
6
|
+
*
|
|
7
|
+
* Each per-phase planner and executor lives in its own module
|
|
8
|
+
* (workspace.ts, consumer-pins.ts, global-install.ts,
|
|
9
|
+
* module-registry.ts); this file just wires them together and owns the
|
|
10
|
+
* top-level flag parsing + dry-run / release-touch / promote special
|
|
11
|
+
* paths.
|
|
12
|
+
*
|
|
13
|
+
* Both `celilo publish ...` (via the CLI dispatcher) and
|
|
14
|
+
* `bun run publish ...` (via the legacy shim at scripts/publish.ts)
|
|
15
|
+
* call `main()` here with their argv slice.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* celilo publish # publish (preflight first)
|
|
19
|
+
* celilo publish --dry-run # preflight + plan, no changes
|
|
20
|
+
* celilo publish --release-touch # auto-fix module stale-drift
|
|
21
|
+
* celilo publish --allow-stale # skip stale-version safeguards
|
|
22
|
+
* celilo publish -y / --yes # auto-confirm prompts
|
|
23
|
+
* celilo publish --alpha # X.Y.Z-alpha.N to @alpha
|
|
24
|
+
* celilo publish --alpha --track-alpha # also force-pin alphas globally
|
|
25
|
+
* celilo publish --alpha --alpha-modules # also publish module +N
|
|
26
|
+
* celilo publish --promote <pkg>@<alpha-version> # graduate alpha to real
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { execSync } from 'node:child_process';
|
|
30
|
+
import { createInterface } from 'node:readline';
|
|
31
|
+
import {
|
|
32
|
+
alphaSkipDecision,
|
|
33
|
+
isAlphaVersion,
|
|
34
|
+
nextAlphaNumber,
|
|
35
|
+
parsePackageSpec,
|
|
36
|
+
stripAlphaSuffix,
|
|
37
|
+
} from './alpha';
|
|
38
|
+
import {
|
|
39
|
+
applyPendingChangesets,
|
|
40
|
+
listPendingChangesets,
|
|
41
|
+
printPendingChangesets,
|
|
42
|
+
} from './changesets';
|
|
43
|
+
import { executePlan } from './execute';
|
|
44
|
+
import { PACKAGES, REPO_ROOT, isPublished, readPkg } from './helpers';
|
|
45
|
+
import { displayPlan, planPublish } from './plan';
|
|
46
|
+
import {
|
|
47
|
+
applyReleaseTouch,
|
|
48
|
+
autoTouchAndCommit,
|
|
49
|
+
printPreflightReport,
|
|
50
|
+
runPreflight,
|
|
51
|
+
} from './preflight';
|
|
52
|
+
import type { PublishMode, PublishOptions } from './types';
|
|
53
|
+
|
|
54
|
+
// ─── Confirm helper ────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
// Set once at main() entry from the resolved argv. Module-scoped because
|
|
57
|
+
// confirm() is called from a dozen places and threading the flag through
|
|
58
|
+
// each path adds noise without lifting any decisions.
|
|
59
|
+
let autoYes = false;
|
|
60
|
+
|
|
61
|
+
function prompt(question: string): Promise<string> {
|
|
62
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
63
|
+
return new Promise((res) =>
|
|
64
|
+
rl.question(question, (a) => {
|
|
65
|
+
rl.close();
|
|
66
|
+
res(a);
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* `-y` / `--yes` auto-confirms every prompt. Doesn't bypass the
|
|
73
|
+
* dirty-tree check or the stale-version safeguard — those are real
|
|
74
|
+
* gates, not just confirmations.
|
|
75
|
+
*/
|
|
76
|
+
async function confirm(question: string): Promise<boolean> {
|
|
77
|
+
if (autoYes) {
|
|
78
|
+
process.stdout.write(`${question}y (auto-confirmed via -y)\n`);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
const reply = await prompt(question);
|
|
82
|
+
return /^[Yy]$/.test(reply.trim());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Flag parsing ──────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
interface ParsedFlags {
|
|
88
|
+
options: PublishOptions;
|
|
89
|
+
dryRun: boolean;
|
|
90
|
+
releaseTouch: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Suppresses the pending-changesets prompt at the top of normal-mode
|
|
93
|
+
* publishes. Use when there are unapplied changesets but the
|
|
94
|
+
* operator wants to ship what's in source right now (or pre-applied
|
|
95
|
+
* the bumps via `bunx changeset version` themselves). The flag has
|
|
96
|
+
* no effect in alpha or promote mode — those skip changesets
|
|
97
|
+
* categorically.
|
|
98
|
+
*/
|
|
99
|
+
skipChangesets: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function parseOptions(argv: string[]): ParsedFlags {
|
|
103
|
+
function takeFlagValue(flag: string): string | null {
|
|
104
|
+
const i = argv.indexOf(flag);
|
|
105
|
+
if (i < 0) return null;
|
|
106
|
+
const value = argv[i + 1];
|
|
107
|
+
if (value === undefined || value.startsWith('-')) {
|
|
108
|
+
console.error(`✗ ${flag} requires a value (e.g. ${flag} @celilo/e2e@0.7.14-alpha.3)`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const allowStale = argv.includes('--allow-stale');
|
|
115
|
+
const yes = argv.includes('-y') || argv.includes('--yes');
|
|
116
|
+
const dryRun = argv.includes('--dry-run') || argv.includes('-n');
|
|
117
|
+
const releaseTouch = argv.includes('--release-touch');
|
|
118
|
+
const alphaFlag = argv.includes('--alpha');
|
|
119
|
+
const trackAlphaFlag = argv.includes('--track-alpha');
|
|
120
|
+
const alphaModulesFlag = argv.includes('--alpha-modules');
|
|
121
|
+
const skipChangesets = argv.includes('--skip-changesets');
|
|
122
|
+
const promoteArg = takeFlagValue('--promote');
|
|
123
|
+
|
|
124
|
+
if (alphaFlag && promoteArg) {
|
|
125
|
+
console.error('✗ --alpha and --promote are mutually exclusive.');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
if (trackAlphaFlag && !alphaFlag) {
|
|
129
|
+
console.error('✗ --track-alpha requires --alpha.');
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
if (alphaModulesFlag && !alphaFlag) {
|
|
133
|
+
console.error('✗ --alpha-modules requires --alpha.');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
if (releaseTouch && (alphaFlag || promoteArg)) {
|
|
137
|
+
console.error('✗ --release-touch cannot be combined with --alpha or --promote.');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const mode: PublishMode = promoteArg
|
|
142
|
+
? { kind: 'promote', target: parsePackageSpec(promoteArg) }
|
|
143
|
+
: alphaFlag
|
|
144
|
+
? { kind: 'alpha', trackAlpha: trackAlphaFlag, alphaModules: alphaModulesFlag }
|
|
145
|
+
: { kind: 'normal' };
|
|
146
|
+
|
|
147
|
+
if (mode.kind === 'promote' && !isAlphaVersion(mode.target.version)) {
|
|
148
|
+
console.error(
|
|
149
|
+
`✗ --promote target "${mode.target.name}@${mode.target.version}" is not an alpha (no -alpha.N suffix).`,
|
|
150
|
+
);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
options: { allowStale, autoYes: yes, mode },
|
|
156
|
+
dryRun,
|
|
157
|
+
releaseTouch,
|
|
158
|
+
skipChangesets,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─── Special-case handlers ─────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
async function handleDryRun(opts: PublishOptions): Promise<never> {
|
|
165
|
+
const report = runPreflight();
|
|
166
|
+
// Match the mode-aware blocking logic the real publish uses.
|
|
167
|
+
const skipWorkspaceStale = opts.mode.kind !== 'normal';
|
|
168
|
+
const skipModuleStale = opts.mode.kind === 'alpha';
|
|
169
|
+
printPreflightReport(report);
|
|
170
|
+
|
|
171
|
+
if (opts.mode.kind === 'alpha') {
|
|
172
|
+
console.log('\nAlpha publish plan:');
|
|
173
|
+
for (const pkg of PACKAGES) {
|
|
174
|
+
const { name, version } = readPkg(pkg);
|
|
175
|
+
if (!name || !version) continue;
|
|
176
|
+
const n = nextAlphaNumber(name, version);
|
|
177
|
+
const decision = alphaSkipDecision(pkg, name, version, n);
|
|
178
|
+
const note = decision.skip ? ` [skip: ${decision.reason}]` : '';
|
|
179
|
+
console.log(` ${name.padEnd(32)} ${version} → ${version}-alpha.${n} (${pkg})${note}`);
|
|
180
|
+
}
|
|
181
|
+
} else if (opts.mode.kind === 'promote') {
|
|
182
|
+
const base = stripAlphaSuffix(opts.mode.target.version);
|
|
183
|
+
console.log(
|
|
184
|
+
`\nPromote plan:\n ${opts.mode.target.name.padEnd(32)} ${opts.mode.target.version} → ${base}`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const anyIssue =
|
|
189
|
+
report.dirty ||
|
|
190
|
+
(!skipWorkspaceStale && report.workspaceStale.length > 0) ||
|
|
191
|
+
(!skipModuleStale && report.moduleStale.length > 0);
|
|
192
|
+
process.exit(anyIssue ? 1 : 0);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function handleReleaseTouch(): Promise<never> {
|
|
196
|
+
const report = runPreflight();
|
|
197
|
+
if (report.dirty) {
|
|
198
|
+
console.error(
|
|
199
|
+
'✗ Working tree is dirty — refusing to touch manifests on top of uncommitted work.',
|
|
200
|
+
);
|
|
201
|
+
console.error(' Commit or stash first, then re-run --release-touch.\n');
|
|
202
|
+
console.error(execSync('git status --short', { encoding: 'utf-8', cwd: REPO_ROOT }));
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
205
|
+
if (report.workspaceStale.length > 0) {
|
|
206
|
+
console.error(
|
|
207
|
+
"✗ Workspace package(s) need real version bumps — these can't be release-touched:",
|
|
208
|
+
);
|
|
209
|
+
for (const i of report.workspaceStale) {
|
|
210
|
+
console.error(` ${i.name}@${i.version} (bump ${i.pkg}/package.json)`);
|
|
211
|
+
}
|
|
212
|
+
console.error(
|
|
213
|
+
'\n Bump those manually, commit, then re-run --release-touch (or just `bun run publish`).',
|
|
214
|
+
);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
applyReleaseTouch(report.moduleStale);
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ─── Main ──────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
export async function main(argv: string[]): Promise<void> {
|
|
224
|
+
// process.chdir matches the prior script-scope behavior: many helpers
|
|
225
|
+
// run `git`, `bun`, `npm` as subprocesses without a cwd, relying on
|
|
226
|
+
// the publisher being at the monorepo root.
|
|
227
|
+
process.chdir(REPO_ROOT);
|
|
228
|
+
|
|
229
|
+
const { options, dryRun, releaseTouch, skipChangesets } = parseOptions(argv);
|
|
230
|
+
autoYes = options.autoYes;
|
|
231
|
+
|
|
232
|
+
if (dryRun) return handleDryRun(options);
|
|
233
|
+
if (releaseTouch) return handleReleaseTouch();
|
|
234
|
+
|
|
235
|
+
// Promote requires the alpha to actually exist on npm — otherwise
|
|
236
|
+
// there's nothing to graduate.
|
|
237
|
+
if (options.mode.kind === 'promote') {
|
|
238
|
+
if (!isPublished(options.mode.target.name, options.mode.target.version)) {
|
|
239
|
+
console.error(
|
|
240
|
+
`✗ Cannot promote: ${options.mode.target.name}@${options.mode.target.version} is not on npm.`,
|
|
241
|
+
);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Preflight cascade: if only module manifests drifted (the common
|
|
247
|
+
// case), offer to auto-touch + commit + continue. For anything else,
|
|
248
|
+
// print and exit.
|
|
249
|
+
const preflight = runPreflight();
|
|
250
|
+
const skipWorkspaceStale = options.mode.kind !== 'normal';
|
|
251
|
+
const skipModuleStale = options.mode.kind === 'alpha';
|
|
252
|
+
const blocking =
|
|
253
|
+
preflight.dirty ||
|
|
254
|
+
(!skipWorkspaceStale && preflight.workspaceStale.length > 0) ||
|
|
255
|
+
(!skipModuleStale && preflight.moduleStale.length > 0);
|
|
256
|
+
|
|
257
|
+
if (blocking && !options.allowStale) {
|
|
258
|
+
const onlyModuleDrift =
|
|
259
|
+
!skipModuleStale &&
|
|
260
|
+
!preflight.dirty &&
|
|
261
|
+
preflight.workspaceStale.length === 0 &&
|
|
262
|
+
preflight.moduleStale.length > 0;
|
|
263
|
+
|
|
264
|
+
if (onlyModuleDrift) {
|
|
265
|
+
printPreflightReport(preflight);
|
|
266
|
+
const proceed = await confirm(
|
|
267
|
+
'\nAuto-touch the drifted manifests, commit, and continue with publish? [y/N] ',
|
|
268
|
+
);
|
|
269
|
+
if (!proceed) {
|
|
270
|
+
console.log(
|
|
271
|
+
'\nAborted. Run `bun run publish --release-touch` to touch without publishing.',
|
|
272
|
+
);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
const { ok, recheck } = autoTouchAndCommit(preflight);
|
|
276
|
+
if (!ok) process.exit(1);
|
|
277
|
+
if (
|
|
278
|
+
recheck &&
|
|
279
|
+
(recheck.dirty || recheck.workspaceStale.length > 0 || recheck.moduleStale.length > 0)
|
|
280
|
+
) {
|
|
281
|
+
console.error('\n✗ Preflight still has issues after auto-touch:');
|
|
282
|
+
printPreflightReport(recheck);
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
console.log(
|
|
286
|
+
`\n✓ Touched and committed ${preflight.moduleStale.length} manifest(s). Continuing with publish.\n`,
|
|
287
|
+
);
|
|
288
|
+
} else {
|
|
289
|
+
printPreflightReport(preflight);
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
} else if (blocking && options.allowStale) {
|
|
293
|
+
console.warn('\n⚠ Preflight issues detected; publishing anyway (--allow-stale):');
|
|
294
|
+
printPreflightReport(preflight);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Changesets — Phase 4 of v2/PUBLILO_CLI.md. Only applies in normal
|
|
298
|
+
// mode: alpha publishes are throwaway prereleases (applying changesets
|
|
299
|
+
// would burn them on alpha publishes), and promote graduates an
|
|
300
|
+
// existing alpha rather than producing a new version. The
|
|
301
|
+
// --skip-changesets flag is the manual escape hatch.
|
|
302
|
+
if (options.mode.kind === 'normal' && !skipChangesets) {
|
|
303
|
+
const pending = listPendingChangesets();
|
|
304
|
+
if (pending.length > 0) {
|
|
305
|
+
printPendingChangesets(pending);
|
|
306
|
+
const apply = await confirm(
|
|
307
|
+
'\nApply changesets (bump versions, update changelogs, commit), then continue with publish? [Y/n] ',
|
|
308
|
+
);
|
|
309
|
+
if (!apply) {
|
|
310
|
+
console.log(
|
|
311
|
+
'\nAborted. Run with --skip-changesets to publish current package.json versions instead.',
|
|
312
|
+
);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
applyPendingChangesets(pending.length);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
console.error(`\n✗ ${err instanceof Error ? err.message : String(err)}`);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
// Re-run preflight on the post-bump tree so we don't proceed
|
|
322
|
+
// with an unexpected state (e.g. changeset version left
|
|
323
|
+
// something dirty that wasn't picked up by our git-add).
|
|
324
|
+
const recheck = runPreflight();
|
|
325
|
+
if (recheck.dirty) {
|
|
326
|
+
console.error('\n✗ Tree is dirty after applying changesets:');
|
|
327
|
+
printPreflightReport(recheck);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
console.log(`\n✓ Applied ${pending.length} changeset(s). Continuing with publish.\n`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const plan = planPublish(options);
|
|
335
|
+
displayPlan(plan);
|
|
336
|
+
|
|
337
|
+
// Alpha mode might have auto-skipped every package — bail before
|
|
338
|
+
// prompting for confirmation we don't need.
|
|
339
|
+
if (options.mode.kind === 'alpha' && plan.workspace.every((p) => p.skipReason !== undefined)) {
|
|
340
|
+
console.log('All packages skipped (no source changes since last alpha). Nothing to do.');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!(await confirm('Proceed? [y/N] '))) {
|
|
345
|
+
console.log('Aborted.');
|
|
346
|
+
process.exit(0);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const result = await executePlan({ plan, confirm });
|
|
350
|
+
|
|
351
|
+
console.log('\n──────────────────────────────────────────────');
|
|
352
|
+
console.log(' Publish summary');
|
|
353
|
+
console.log('──────────────────────────────────────────────');
|
|
354
|
+
if (result.published.length) {
|
|
355
|
+
console.log('Published:');
|
|
356
|
+
for (const p of result.published) console.log(` ✓ ${p.name}@${p.version}`);
|
|
357
|
+
}
|
|
358
|
+
if (result.skipped.length) {
|
|
359
|
+
console.log('Skipped:');
|
|
360
|
+
for (const s of result.skipped) console.log(` - ${s}`);
|
|
361
|
+
}
|
|
362
|
+
if (!result.published.length && !result.skipped.length) {
|
|
363
|
+
console.log('Nothing to do.');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-registry planner test — Phase 4 is the slimmest publish
|
|
3
|
+
* phase (revision selection lives inside `celilo module publish`),
|
|
4
|
+
* so the planner is a thin wrap around `listModuleDirs`. We test
|
|
5
|
+
* the pure inner `mapModuleDirsToItems` directly so the test surface
|
|
6
|
+
* doesn't need to mock `./helpers` — mocking that path leaks to
|
|
7
|
+
* unrelated tests via bun:test's process-global mock.module.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test } from 'bun:test';
|
|
11
|
+
import { mapModuleDirsToItems } from './module-registry';
|
|
12
|
+
|
|
13
|
+
describe('mapModuleDirsToItems', () => {
|
|
14
|
+
test('empty input → empty plan', () => {
|
|
15
|
+
expect(mapModuleDirsToItems([])).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('wraps each discovered module dir in a ModuleItem', () => {
|
|
19
|
+
expect(
|
|
20
|
+
mapModuleDirsToItems([
|
|
21
|
+
'/repo/modules/caddy',
|
|
22
|
+
'/repo/modules/homebridge',
|
|
23
|
+
'/repo/modules/dns-external',
|
|
24
|
+
]),
|
|
25
|
+
).toEqual([
|
|
26
|
+
{ moduleDir: '/repo/modules/caddy' },
|
|
27
|
+
{ moduleDir: '/repo/modules/homebridge' },
|
|
28
|
+
{ moduleDir: '/repo/modules/dns-external' },
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('preserves input order', () => {
|
|
33
|
+
const result = mapModuleDirsToItems(['/repo/modules/z', '/repo/modules/a', '/repo/modules/m']);
|
|
34
|
+
expect(result.map((m) => m.moduleDir)).toEqual([
|
|
35
|
+
'/repo/modules/z',
|
|
36
|
+
'/repo/modules/a',
|
|
37
|
+
'/repo/modules/m',
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module registry publish — Phase 4 of `celilo publish`.
|
|
3
|
+
*
|
|
4
|
+
* Phase 4 is the slimmest phase: revision selection and tarball
|
|
5
|
+
* construction live inside `celilo module publish` itself, so the plan
|
|
6
|
+
* only enumerates which module directories to ship. The executor
|
|
7
|
+
* shells out to a single `celilo module publish <dirs...>` invocation
|
|
8
|
+
* (using the local source, not the global install, so this script
|
|
9
|
+
* always runs the just-built CLI from this repo).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
13
|
+
import { existsSync } from 'node:fs';
|
|
14
|
+
import { join, relative } from 'node:path';
|
|
15
|
+
import { REPO_ROOT, listModuleDirs } from './helpers';
|
|
16
|
+
import type { ModuleItem } from './types';
|
|
17
|
+
|
|
18
|
+
// ─── Planner ───────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Pure wrapper: turn a list of module directories into the
|
|
22
|
+
* corresponding `ModuleItem[]`. Exists so the test surface can
|
|
23
|
+
* exercise the wrapping contract without mocking `listModuleDirs`
|
|
24
|
+
* (which lives in helpers.ts and is process-globally referenced by
|
|
25
|
+
* other modules — mocking it would leak to unrelated tests).
|
|
26
|
+
*/
|
|
27
|
+
export function mapModuleDirsToItems(dirs: string[]): ModuleItem[] {
|
|
28
|
+
return dirs.map((moduleDir) => ({ moduleDir }));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function planModulePublish(): ModuleItem[] {
|
|
32
|
+
return mapModuleDirsToItems(listModuleDirs());
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─── Executor ──────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
export function executeModulePublish(items: ModuleItem[], opts: { allowStale: boolean }): void {
|
|
38
|
+
if (items.length === 0) {
|
|
39
|
+
console.log('\nNo modules with manifest.yml found — skipping module-publish phase.');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const modulesRoot = join(REPO_ROOT, 'modules');
|
|
44
|
+
if (!existsSync(modulesRoot)) {
|
|
45
|
+
console.log('\nNo modules/ directory at repo root — skipping module-publish phase.');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log('\n──────────────────────────────────────────────');
|
|
50
|
+
console.log(' Publishing modules to celilo registry');
|
|
51
|
+
console.log('──────────────────────────────────────────────');
|
|
52
|
+
for (const m of items) console.log(` ${relative(REPO_ROOT, m.moduleDir)}`);
|
|
53
|
+
console.log();
|
|
54
|
+
|
|
55
|
+
const cliEntry = join(REPO_ROOT, 'apps', 'celilo', 'src', 'cli', 'index.ts');
|
|
56
|
+
const args = ['run', cliEntry, 'module', 'publish', ...items.map((m) => m.moduleDir)];
|
|
57
|
+
if (opts.allowStale) args.push('--allow-stale');
|
|
58
|
+
|
|
59
|
+
const r = spawnSync('bun', args, { cwd: REPO_ROOT, stdio: 'inherit' });
|
|
60
|
+
if (r.status !== 0) {
|
|
61
|
+
console.error(`\n✗ Module publish failed (exit ${r.status}). Aborting.`);
|
|
62
|
+
process.exit(r.status ?? 1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Umbrella planner — composes the per-phase planners into a single
|
|
3
|
+
* `PublishPlan`.
|
|
4
|
+
*
|
|
5
|
+
* The planner is the value-add of v2/PUBLILO_CLI.md Phase 2: it does
|
|
6
|
+
* all the world-reads (npm view, git log, fs walks) and produces a
|
|
7
|
+
* typed description of every side effect the executor will perform.
|
|
8
|
+
* Dry-run prints the plan and exits; a real publish confirms the plan
|
|
9
|
+
* and hands it to the executor.
|
|
10
|
+
*
|
|
11
|
+
* Per-phase planners are imported lazily where needed so unit tests
|
|
12
|
+
* can mock individual phases without dragging in the others' I/O.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { planConsumerPins } from './consumer-pins';
|
|
16
|
+
import { planGlobalUpdate } from './global-install';
|
|
17
|
+
import { PACKAGES, buildWorkspaceVersionMap, currentGitHead } from './helpers';
|
|
18
|
+
import { planModulePublish } from './module-registry';
|
|
19
|
+
import { runPreflight } from './preflight';
|
|
20
|
+
import type { PublishOptions, PublishPlan } from './types';
|
|
21
|
+
import { planWorkspace } from './workspace';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build the full publish plan. Reads from the world (preflight, npm,
|
|
25
|
+
* git, fs); doesn't mutate.
|
|
26
|
+
*
|
|
27
|
+
* Phase gating per the spec:
|
|
28
|
+
* - normal: all four phases planned.
|
|
29
|
+
* - alpha: workspace planned; consumer pins SKIPPED (consumers
|
|
30
|
+
* opt into @alpha manually); global update SKIPPED unless
|
|
31
|
+
* --track-alpha; module publish SKIPPED unless
|
|
32
|
+
* --alpha-modules.
|
|
33
|
+
* - promote: all four phases planned — this IS a real release.
|
|
34
|
+
*
|
|
35
|
+
* In alpha mode the consumer-pin / global-update / module-publish
|
|
36
|
+
* planners are simply not invoked, so their items stay empty in the
|
|
37
|
+
* returned plan. The executor reads the same flag combination to know
|
|
38
|
+
* which phases to actually run; the empty-list contract means "nothing
|
|
39
|
+
* to do here" either way.
|
|
40
|
+
*/
|
|
41
|
+
export function planPublish(opts: PublishOptions): PublishPlan {
|
|
42
|
+
const preflight = runPreflight();
|
|
43
|
+
const baseWorkspaceVersions = buildWorkspaceVersionMap();
|
|
44
|
+
const gitHead = currentGitHead();
|
|
45
|
+
|
|
46
|
+
const workspaceResult = planWorkspace({
|
|
47
|
+
mode: opts.mode,
|
|
48
|
+
packages: PACKAGES,
|
|
49
|
+
baseWorkspaceVersions,
|
|
50
|
+
gitHead,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// PROJECTED publish list — what executeWorkspace would land on
|
|
54
|
+
// npm if no per-package skips happen at execution time. Used to
|
|
55
|
+
// seed the global-update plan with force-pin targets.
|
|
56
|
+
const projectedPublishes = workspaceResult.items
|
|
57
|
+
.filter((item) => !item.skipReason)
|
|
58
|
+
.map((item) => ({ name: item.name, version: item.versionToPublish }));
|
|
59
|
+
|
|
60
|
+
const runPhase2 = opts.mode.kind !== 'alpha';
|
|
61
|
+
const runPhase3 = opts.mode.kind !== 'alpha' || opts.mode.trackAlpha;
|
|
62
|
+
const runPhase4 =
|
|
63
|
+
opts.mode.kind === 'normal' ||
|
|
64
|
+
opts.mode.kind === 'promote' ||
|
|
65
|
+
(opts.mode.kind === 'alpha' && opts.mode.alphaModules);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
mode: opts.mode,
|
|
69
|
+
options: opts,
|
|
70
|
+
preflight,
|
|
71
|
+
workspace: workspaceResult.items,
|
|
72
|
+
consumerPins: runPhase2 ? planConsumerPins() : [],
|
|
73
|
+
globalUpdate: runPhase3
|
|
74
|
+
? planGlobalUpdate({
|
|
75
|
+
justPublished: projectedPublishes,
|
|
76
|
+
trackAlpha: opts.mode.kind === 'alpha' && opts.mode.trackAlpha,
|
|
77
|
+
})
|
|
78
|
+
: [],
|
|
79
|
+
modulePublish: runPhase4 ? planModulePublish() : [],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Print the plan for the operator. Called before any side effects (in
|
|
85
|
+
* both --dry-run and real publish), so confirmation is informed.
|
|
86
|
+
*/
|
|
87
|
+
export function displayPlan(plan: PublishPlan): void {
|
|
88
|
+
const mode = plan.mode;
|
|
89
|
+
const planLabel =
|
|
90
|
+
mode.kind === 'alpha'
|
|
91
|
+
? 'Alpha publish plan'
|
|
92
|
+
: mode.kind === 'promote'
|
|
93
|
+
? 'Promote plan'
|
|
94
|
+
: 'Publish plan';
|
|
95
|
+
console.log(`${planLabel} (in this order):\n`);
|
|
96
|
+
for (const p of plan.workspace) {
|
|
97
|
+
const arrow =
|
|
98
|
+
mode.kind === 'promote'
|
|
99
|
+
? `${mode.target.version} → ${p.versionToPublish}`
|
|
100
|
+
: p.versionToPublish === p.baseVersion
|
|
101
|
+
? p.versionToPublish
|
|
102
|
+
: `${p.baseVersion} → ${p.versionToPublish}`;
|
|
103
|
+
const note = p.skipReason ? ` [skip: ${p.skipReason}]` : '';
|
|
104
|
+
console.log(` ${p.name.padEnd(32)} ${arrow} (${p.pkg})${note}`);
|
|
105
|
+
}
|
|
106
|
+
console.log();
|
|
107
|
+
}
|