@celilo/cli 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +7 -4
- package/CELILO_CORE_MODULES.md +7 -6
- package/CELILO_SUBSYSTEMS.md +52 -6
- package/MODULE_PRIMITIVES.md +164 -0
- package/README.md +5 -5
- package/drizzle/0015_port_forwards.sql +12 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +13 -5
- package/schemas/system_config.json +1 -1
- package/src/ansible/inventory.test.ts +1 -1
- package/src/ansible/inventory.ts +3 -3
- package/src/api/protocol.test.ts +1 -1
- package/src/api/remote-client.test.ts +1 -1
- package/src/api/serve.ts +5 -5
- package/src/capabilities/public-web-publish.test.ts +209 -0
- package/src/capabilities/validation.test.ts +1 -1
- package/src/capabilities/well-known.test.ts +1 -1
- package/src/capabilities/well-known.ts +2 -2
- package/src/cli/command-tree-parser.ts +3 -1
- package/src/cli/commands/api.ts +1 -1
- package/src/cli/commands/apt-upgrade.test.ts +33 -0
- package/src/cli/commands/apt-upgrade.ts +63 -0
- package/src/cli/commands/backup-pull.ts +1 -1
- package/src/cli/commands/commands-json.ts +29 -0
- package/src/cli/commands/completion.ts +1 -1
- package/src/cli/commands/events.ts +1 -1
- package/src/cli/commands/module-changeset.ts +1 -1
- package/src/cli/commands/module-import.ts +1 -1
- package/src/cli/commands/module-list.ts +16 -2
- package/src/cli/commands/module-publish.ts +1 -1
- package/src/cli/commands/module-remove.ts +26 -1
- package/src/cli/commands/module-types.ts +1 -1
- package/src/cli/commands/module-upgrade.ts +1 -1
- package/src/cli/commands/module-version.ts +1 -1
- package/src/cli/commands/module-where.test.ts +26 -0
- package/src/cli/commands/module-where.ts +130 -0
- package/src/cli/commands/proxmox-node-list.ts +1 -1
- package/src/cli/commands/publish/alpha.test.ts +1 -1
- package/src/cli/commands/publish/alpha.ts +3 -3
- package/src/cli/commands/publish/changesets.ts +1 -1
- package/src/cli/commands/publish/global-install.ts +6 -2
- package/src/cli/commands/publish/helpers.ts +21 -14
- package/src/cli/commands/publish/index.ts +4 -4
- package/src/cli/commands/publish/plan.ts +3 -3
- package/src/cli/commands/publish/preflight.ts +2 -2
- package/src/cli/commands/publish/types.ts +2 -2
- package/src/cli/commands/publish/workspace.test.ts +1 -1
- package/src/cli/commands/publish/workspace.ts +2 -2
- package/src/cli/commands/registry-owner.test.ts +166 -0
- package/src/cli/commands/registry-owner.ts +124 -0
- package/src/cli/commands/registry-token.test.ts +109 -0
- package/src/cli/commands/registry-token.ts +194 -0
- package/src/cli/commands/restore.ts +1 -1
- package/src/cli/commands/service-add-proxmox.ts +1 -1
- package/src/cli/commands/service-list.ts +15 -2
- package/src/cli/commands/subscribers-list.ts +1 -1
- package/src/cli/commands/system-apply-config-equivalence.test.ts +1 -1
- package/src/cli/commands/system-apply-config.ts +1 -1
- package/src/cli/commands/system-init-deprecation.test.ts +1 -1
- package/src/cli/commands/system-init.ts +2 -2
- package/src/cli/commands/token.test.ts +26 -0
- package/src/cli/commands/token.ts +160 -0
- package/src/cli/completion.ts +21 -0
- package/src/cli/generate-zsh-completion.test.ts +22 -4
- package/src/cli/generate-zsh-completion.ts +7 -3
- package/src/cli/index.ts +170 -7
- package/src/cli/parser.ts +1 -1
- package/src/cli/restore-command.test.ts +1 -1
- package/src/db/client.ts +1 -1
- package/src/db/schema.ts +50 -7
- package/src/hooks/capability-loader-firewall.test.ts +4 -3
- package/src/hooks/capability-loader.ts +100 -10
- package/src/hooks/define-hook.test.ts +24 -0
- package/src/hooks/executor.test.ts +1 -1
- package/src/hooks/executor.ts +2 -2
- package/src/hooks/types.ts +1 -1
- package/src/manifest/contracts/v1.ts +2 -2
- package/src/manifest/schema.ts +13 -13
- package/src/manifest/template-validator.ts +1 -1
- package/src/module/packaging/build.ts +19 -0
- package/src/module/packaging/workspace-deps.test.ts +94 -0
- package/src/module/packaging/workspace-deps.ts +185 -0
- package/src/module/versioning/changeset-version.ts +1 -1
- package/src/policy/no-hand-built-ssh.test.ts +90 -0
- package/src/registry/client.test.ts +86 -0
- package/src/registry/client.ts +67 -1
- package/src/services/api-access.ts +1 -1
- package/src/services/aspect-approvals.ts +1 -1
- package/src/services/aspect-runner.ts +3 -3
- package/src/services/aspect-template-resolver.test.ts +1 -1
- package/src/services/aspect-template-resolver.ts +1 -1
- package/src/services/build-bus/delivery-events.ts +1 -1
- package/src/services/build-bus/fan-out.ts +1 -1
- package/src/services/build-bus/hook-dispatch.ts +1 -1
- package/src/services/build-bus/receiver-server.ts +1 -1
- package/src/services/build-bus/status.test.ts +1 -1
- package/src/services/build-bus/status.ts +1 -1
- package/src/services/build-bus/subscriber-store.ts +1 -1
- package/src/services/bus-interview.ts +1 -1
- package/src/services/celilo-events.ts +1 -1
- package/src/services/celilo-mgmt-hooks.test.ts +1 -1
- package/src/services/cross-module-read.ts +1 -1
- package/src/services/deploy-posture.ts +1 -1
- package/src/services/deployed-systems.test.ts +1 -1
- package/src/services/deployed-systems.ts +4 -4
- package/src/services/dns-provider-backfill.ts +2 -2
- package/src/services/events-daemon.ts +1 -1
- package/src/services/machine-pool.ts +3 -3
- package/src/services/module-deploy.ts +93 -8
- package/src/services/module-subscriptions.ts +1 -1
- package/src/services/module-types-generator.ts +1 -1
- package/src/services/module-validator/git-hygiene.ts +1 -1
- package/src/services/port-forwards.test.ts +92 -0
- package/src/services/port-forwards.ts +86 -0
- package/src/services/proxmox-reconcile.ts +2 -2
- package/src/services/public-web-republish.test.ts +189 -0
- package/src/services/public-web-republish.ts +84 -0
- package/src/services/restore-from-file.ts +1 -1
- package/src/services/restore-preflight.ts +1 -1
- package/src/services/system-identity.ts +1 -1
- package/src/services/system-init.test.ts +2 -2
- package/src/services/system-init.ts +2 -2
- package/src/services/web-route-cleanup.test.ts +250 -0
- package/src/services/web-route-cleanup.ts +144 -0
- package/src/templates/generator.ts +2 -2
- package/src/types/infrastructure.ts +1 -1
- package/src/variables/computed/computed-integration.test.ts +1 -1
- package/src/variables/computed/evaluate.ts +1 -1
- package/src/variables/computed/parse.ts +1 -1
- package/src/variables/context.test.ts +1 -1
- package/src/variables/context.ts +5 -5
- package/src/variables/lxc-nameserver.test.ts +1 -1
- package/src/variables/resolver.ts +2 -2
- package/src/variables/types.ts +1 -1
- package/src/api/protocol.ts +0 -159
- package/src/api/remote-client.ts +0 -218
- package/src/cli/command-registry.ts +0 -1488
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module where command — host discovery: which system(s) serve a module.
|
|
3
|
+
*
|
|
4
|
+
* Answers "which host is providing services for <module>?" without an ssh
|
|
5
|
+
* scavenger hunt (ce-jje). Data source: the deployed-systems layer
|
|
6
|
+
* (module_systems) for the addressable hosts, reconciled live against Proxmox
|
|
7
|
+
* for the real node. Reachability is a role-based hint derived from the zone —
|
|
8
|
+
* never a hardcoded address (see CLAUDE.md network-model rules).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { eq } from 'drizzle-orm';
|
|
12
|
+
import { getDb } from '../../db/client';
|
|
13
|
+
import type { NetworkZone } from '../../db/schema';
|
|
14
|
+
import { modules } from '../../db/schema';
|
|
15
|
+
import { getModuleSystems } from '../../services/deployed-systems';
|
|
16
|
+
import { formatPlacementLine, reconcilePlacement } from '../../services/placement-reconcile';
|
|
17
|
+
import { getArg, hasFlag, validateRequiredArgs } from '../parser';
|
|
18
|
+
import type { CommandResult } from '../types';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Role-based reachability hint for a zone. Says HOW to reach a host by the
|
|
22
|
+
* zone's firewall posture, not a literal address (subnets are operator config).
|
|
23
|
+
*/
|
|
24
|
+
export function reachabilityHint(zone: NetworkZone | string): string {
|
|
25
|
+
switch (zone) {
|
|
26
|
+
case 'internal':
|
|
27
|
+
return 'reachable directly from the internal/home LAN';
|
|
28
|
+
case 'external':
|
|
29
|
+
return 'public zone — reachable at its own address (cloud/VPS)';
|
|
30
|
+
case 'dmz':
|
|
31
|
+
case 'app':
|
|
32
|
+
case 'secure':
|
|
33
|
+
return `firewall-segmented (${zone}) — reach via the firewall's natIp DNAT, not routable directly from the LAN`;
|
|
34
|
+
default:
|
|
35
|
+
return `zone ${zone}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Handle module where command.
|
|
41
|
+
*
|
|
42
|
+
* Usage: celilo module where <id> [--json]
|
|
43
|
+
*
|
|
44
|
+
* @param args - Command arguments (module id)
|
|
45
|
+
* @param flags - Command flags (--json)
|
|
46
|
+
* @returns Command result
|
|
47
|
+
*/
|
|
48
|
+
export async function handleModuleWhere(
|
|
49
|
+
args: string[],
|
|
50
|
+
flags: Record<string, string | boolean> = {},
|
|
51
|
+
): Promise<CommandResult> {
|
|
52
|
+
const error = validateRequiredArgs(args, 1);
|
|
53
|
+
if (error) {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
error: `${error}\n\nUsage: celilo module where <id> [--json]`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const moduleId = getArg(args, 0);
|
|
61
|
+
if (!moduleId) {
|
|
62
|
+
return { success: false, error: 'Module ID is required' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const db = getDb();
|
|
66
|
+
const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
|
|
67
|
+
|
|
68
|
+
if (!module) {
|
|
69
|
+
// CI/build infra (the celilo-builder VM, Forgejo runners) is NOT a module
|
|
70
|
+
// and is not in module_systems — point the operator at where that topology
|
|
71
|
+
// actually lives rather than a bare "not found".
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
error: `Module not found: ${moduleId}\n\nHost discovery covers deployed modules only. CI/build infrastructure (the celilo-builder VM, Forgejo CI runners) is not tracked here — see openspec/specs/forgejo-runner/spec.md and openspec/changes/build-bus-poll-cd/proposal.md for that topology.`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const systems = getModuleSystems(moduleId, db);
|
|
79
|
+
|
|
80
|
+
if (systems.length === 0) {
|
|
81
|
+
const message = `Module '${moduleId}' has no deployed systems (API-only module, or not yet deployed).`;
|
|
82
|
+
if (hasFlag(flags, 'json')) {
|
|
83
|
+
return {
|
|
84
|
+
success: true,
|
|
85
|
+
message: JSON.stringify({ module: moduleId, systems: [] }, null, 2),
|
|
86
|
+
rawOutput: true,
|
|
87
|
+
data: { module: moduleId, systems: [] },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return { success: true, message };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Reconcile the real node live from Proxmox (never throws; degrades to
|
|
94
|
+
// "node unknown" on outage) — same live-placement source as module status.
|
|
95
|
+
const placements = await reconcilePlacement(systems);
|
|
96
|
+
|
|
97
|
+
const data = placements.map(({ system, resolution }) => ({
|
|
98
|
+
name: system.name,
|
|
99
|
+
hostname: system.hostname,
|
|
100
|
+
ipv4_address: system.ipv4_address,
|
|
101
|
+
zone: system.zone,
|
|
102
|
+
vmid: system.infrastructure.vmid ?? null,
|
|
103
|
+
infra_type: system.infrastructure.type,
|
|
104
|
+
placement: formatPlacementLine(system, resolution),
|
|
105
|
+
reachability: reachabilityHint(system.zone),
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
if (hasFlag(flags, 'json')) {
|
|
109
|
+
return {
|
|
110
|
+
success: true,
|
|
111
|
+
message: JSON.stringify({ module: moduleId, systems: data }, null, 2),
|
|
112
|
+
rawOutput: true,
|
|
113
|
+
data: { module: moduleId, systems: data },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const lines = [`Module '${moduleId}' is served by:`, ''];
|
|
118
|
+
for (const sys of data) {
|
|
119
|
+
lines.push(` ${sys.hostname} (${sys.ipv4_address})`);
|
|
120
|
+
lines.push(` placement: ${sys.placement}`);
|
|
121
|
+
lines.push(` reachability: ${sys.reachability}`);
|
|
122
|
+
lines.push('');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
success: true,
|
|
127
|
+
message: lines.join('\n').trimEnd(),
|
|
128
|
+
data: { module: moduleId, systems: data },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `celilo proxmox node list` — live per-node capacity from the Proxmox cluster
|
|
3
|
-
* (ISS-0060, Phase 1 of
|
|
3
|
+
* (ISS-0060, Phase 1 of openspec/changes/proxmox-capacity-lifecycle/proposal.md).
|
|
4
4
|
*
|
|
5
5
|
* Reads reality from the Proxmox API via `ProxmoxClient`, never a cached DB
|
|
6
6
|
* value — the foundation for capacity-aware placement (ISS-0061) and
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Alpha-helper tests —
|
|
2
|
+
* Alpha-helper tests — openspec/changes/publilo-cli/proposal.md Phase 3 payoff.
|
|
3
3
|
*
|
|
4
4
|
* The pure functions in alpha.ts (parsePackageSpec, stripAlphaSuffix,
|
|
5
5
|
* isAlphaVersion, pickNextAlphaN, decideAlphaSkip) need no I/O to
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Alpha-tagging logic — `--alpha` publish + `--promote` graduation.
|
|
3
3
|
*
|
|
4
|
-
* The semantics live in `
|
|
4
|
+
* The semantics live in `openspec/specs/alpha-publish-tagging/spec.md`; this
|
|
5
5
|
* module is the implementation surface those flags drive. Per
|
|
6
|
-
*
|
|
6
|
+
* openspec/changes/publilo-cli/proposal.md Phase 3, the alpha-specific helpers live here
|
|
7
7
|
* (not in helpers.ts) so it's clear where to look when extending the
|
|
8
|
-
* model — e.g. to add `--beta` once `
|
|
8
|
+
* model — e.g. to add `--beta` once `openspec/changes/build-bus-poll-cd/proposal.md`'s validation
|
|
9
9
|
* signal lands.
|
|
10
10
|
*
|
|
11
11
|
* Two-layer design for testability:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @changesets/cli integration —
|
|
2
|
+
* @changesets/cli integration — openspec/changes/publilo-cli/proposal.md Phase 4.
|
|
3
3
|
*
|
|
4
4
|
* Changesets is the "what versions does this publish produce" piece
|
|
5
5
|
* of the workflow. During dev, the operator runs `bun changeset` to
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { spawnSync } from 'node:child_process';
|
|
18
18
|
import { existsSync, readFileSync } from 'node:fs';
|
|
19
19
|
import { join } from 'node:path';
|
|
20
|
-
import {
|
|
20
|
+
import { getPublishPackages, readGlobalInstalledVersion, readPkg } from './helpers';
|
|
21
21
|
import type { GlobalUpdateItem, PackageJson } from './types';
|
|
22
22
|
|
|
23
23
|
// ─── Planner ───────────────────────────────────────────────────────
|
|
@@ -97,7 +97,11 @@ export function planGlobalUpdate(input: PlanGlobalUpdateInput): GlobalUpdateItem
|
|
|
97
97
|
return [];
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
const ourNames = new Set(
|
|
100
|
+
const ourNames = new Set(
|
|
101
|
+
getPublishPackages()
|
|
102
|
+
.map((p) => readPkg(p).name)
|
|
103
|
+
.filter((n): n is string => !!n),
|
|
104
|
+
);
|
|
101
105
|
const installed = Object.keys(globalPkg.dependencies ?? {}).filter((n) => ourNames.has(n));
|
|
102
106
|
|
|
103
107
|
return decideGlobalUpdates({
|
|
@@ -13,19 +13,9 @@
|
|
|
13
13
|
import { execSync, spawnSync } from 'node:child_process';
|
|
14
14
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
15
15
|
import { join, resolve } from 'node:path';
|
|
16
|
+
import { publishableSet, readWorkspace } from '../../../../../../scripts/workspace-graph';
|
|
16
17
|
import type { PackageJson, WorkspaceVersionMap } from './types';
|
|
17
18
|
|
|
18
|
-
// Publish in dependency order: leaves first, then consumers.
|
|
19
|
-
// Exported so tests can construct a synthetic packages list against the
|
|
20
|
-
// same shape; runtime always uses these in this exact order.
|
|
21
|
-
export const PACKAGES = [
|
|
22
|
-
'packages/cli-display',
|
|
23
|
-
'packages/capabilities',
|
|
24
|
-
'packages/event-bus',
|
|
25
|
-
'apps/celilo',
|
|
26
|
-
'packages/e2e',
|
|
27
|
-
] as const;
|
|
28
|
-
|
|
29
19
|
// REPO_ROOT discovery — this file lives at
|
|
30
20
|
// apps/celilo/src/cli/commands/publish/helpers.ts, six dirs deep.
|
|
31
21
|
// Kept as a relative path (matches existing convention in
|
|
@@ -35,6 +25,23 @@ export const REPO_ROOT = resolve(import.meta.dir, '../../../../../..');
|
|
|
35
25
|
|
|
36
26
|
export const ENV_FILE = join(REPO_ROOT, '.env');
|
|
37
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The publishable workspace packages (repo-relative dirs), topologically
|
|
30
|
+
* ordered leaves-first — the order the executor publishes in, so a dep is on
|
|
31
|
+
* npm before its dependent references it. DERIVED from the live workspace
|
|
32
|
+
* (every non-private @celilo/* package), never a hardcoded list: a hand-list is
|
|
33
|
+
* exactly what left @celilo/core/mcp/visualizer invisible to the driver and let
|
|
34
|
+
* npm-consumer-smoke rot (apps/celilo/designs/WORKSPACE_PACKAGE_GRAPH.md).
|
|
35
|
+
*
|
|
36
|
+
* A FUNCTION, not a module-load const: the publish command ships in the
|
|
37
|
+
* consumer @celilo/cli, and readWorkspace() against a consumer's non-monorepo
|
|
38
|
+
* install would throw at import — bricking the whole CLI. Deriving lazily means
|
|
39
|
+
* the disk read happens only when a publish actually runs (in the monorepo).
|
|
40
|
+
*/
|
|
41
|
+
export function getPublishPackages(): string[] {
|
|
42
|
+
return publishableSet(readWorkspace(REPO_ROOT)).map((p) => p.dir);
|
|
43
|
+
}
|
|
44
|
+
|
|
38
45
|
export function readPkg(dir: string): PackageJson {
|
|
39
46
|
return JSON.parse(readFileSync(join(REPO_ROOT, dir, 'package.json'), 'utf-8'));
|
|
40
47
|
}
|
|
@@ -76,7 +83,7 @@ export function isAncestor(maybeAncestor: string, descendant: string): boolean {
|
|
|
76
83
|
|
|
77
84
|
export function buildWorkspaceVersionMap(): WorkspaceVersionMap {
|
|
78
85
|
const m = new Map<string, string>();
|
|
79
|
-
for (const pkg of
|
|
86
|
+
for (const pkg of getPublishPackages()) {
|
|
80
87
|
const { name, version } = readPkg(pkg);
|
|
81
88
|
if (name && version) m.set(name, version);
|
|
82
89
|
}
|
|
@@ -129,7 +136,7 @@ export function readExternalProjectPaths(): string[] {
|
|
|
129
136
|
* Read `NPM_PUBLISH_TARGET` from .env — the registry URL `celilo publish`
|
|
130
137
|
* points `bun publish --registry` at for @celilo/* packages (a deployed
|
|
131
138
|
* npm-cache-node). Unset → publish to npmjs (current behavior). See
|
|
132
|
-
*
|
|
139
|
+
* openspec/changes/private-npm-registry/proposal.md Phase 3.1 / openspec/changes/publilo-cli/proposal.md decision 10.
|
|
133
140
|
*/
|
|
134
141
|
export function readNpmPublishTarget(): string | null {
|
|
135
142
|
if (!existsSync(ENV_FILE)) return null;
|
|
@@ -230,7 +237,7 @@ export function readGlobalInstalledVersion(name: string): string | null {
|
|
|
230
237
|
*/
|
|
231
238
|
export function fetchLatestVersions(): Map<string, string> {
|
|
232
239
|
const out = new Map<string, string>();
|
|
233
|
-
for (const pkg of
|
|
240
|
+
for (const pkg of getPublishPackages()) {
|
|
234
241
|
const { name } = readPkg(pkg);
|
|
235
242
|
if (!name) continue;
|
|
236
243
|
const r = spawnSync('npm', ['view', name, 'version'], {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `celilo publish` entry point — thin orchestration over the
|
|
3
|
-
* planner/executor split (
|
|
3
|
+
* planner/executor split (openspec/changes/publilo-cli/proposal.md Phase 2).
|
|
4
4
|
*
|
|
5
5
|
* parse args → preflight → build plan → display → confirm → execute
|
|
6
6
|
*
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
printPendingChangesets,
|
|
42
42
|
} from './changesets';
|
|
43
43
|
import { executePlan } from './execute';
|
|
44
|
-
import {
|
|
44
|
+
import { REPO_ROOT, getPublishPackages, isPublished, readPkg } from './helpers';
|
|
45
45
|
import { displayPlan, planPublish } from './plan';
|
|
46
46
|
import {
|
|
47
47
|
applyReleaseTouch,
|
|
@@ -170,7 +170,7 @@ async function handleDryRun(opts: PublishOptions): Promise<never> {
|
|
|
170
170
|
|
|
171
171
|
if (opts.mode.kind === 'alpha') {
|
|
172
172
|
console.log('\nAlpha publish plan:');
|
|
173
|
-
for (const pkg of
|
|
173
|
+
for (const pkg of getPublishPackages()) {
|
|
174
174
|
const { name, version } = readPkg(pkg);
|
|
175
175
|
if (!name || !version) continue;
|
|
176
176
|
const n = nextAlphaNumber(name, version);
|
|
@@ -294,7 +294,7 @@ export async function main(argv: string[]): Promise<void> {
|
|
|
294
294
|
printPreflightReport(preflight);
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
// Changesets — Phase 4 of
|
|
297
|
+
// Changesets — Phase 4 of openspec/changes/publilo-cli/proposal.md. Only applies in normal
|
|
298
298
|
// mode: alpha publishes are throwaway prereleases (applying changesets
|
|
299
299
|
// would burn them on alpha publishes), and promote graduates an
|
|
300
300
|
// existing alpha rather than producing a new version. The
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Umbrella planner — composes the per-phase planners into a single
|
|
3
3
|
* `PublishPlan`.
|
|
4
4
|
*
|
|
5
|
-
* The planner is the value-add of
|
|
5
|
+
* The planner is the value-add of openspec/changes/publilo-cli/proposal.md Phase 2: it does
|
|
6
6
|
* all the world-reads (npm view, git log, fs walks) and produces a
|
|
7
7
|
* typed description of every side effect the executor will perform.
|
|
8
8
|
* Dry-run prints the plan and exits; a real publish confirms the plan
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { planConsumerPins } from './consumer-pins';
|
|
16
16
|
import { planGlobalUpdate } from './global-install';
|
|
17
|
-
import {
|
|
17
|
+
import { buildWorkspaceVersionMap, currentGitHead, getPublishPackages } from './helpers';
|
|
18
18
|
import { planModulePublish } from './module-registry';
|
|
19
19
|
import { runPreflight } from './preflight';
|
|
20
20
|
import type { PublishOptions, PublishPlan } from './types';
|
|
@@ -45,7 +45,7 @@ export function planPublish(opts: PublishOptions): PublishPlan {
|
|
|
45
45
|
|
|
46
46
|
const workspaceResult = planWorkspace({
|
|
47
47
|
mode: opts.mode,
|
|
48
|
-
packages:
|
|
48
|
+
packages: getPublishPackages(),
|
|
49
49
|
baseWorkspaceVersions,
|
|
50
50
|
gitHead,
|
|
51
51
|
});
|
|
@@ -17,8 +17,8 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
17
17
|
import { join, relative } from 'node:path';
|
|
18
18
|
import { checkModuleStale } from '../../../services/module-validator/git-hygiene';
|
|
19
19
|
import {
|
|
20
|
-
PACKAGES,
|
|
21
20
|
REPO_ROOT,
|
|
21
|
+
getPublishPackages,
|
|
22
22
|
isAncestor,
|
|
23
23
|
isPublished,
|
|
24
24
|
lastCommitTouching,
|
|
@@ -77,7 +77,7 @@ export function runPreflight(): PreflightReport {
|
|
|
77
77
|
}).trim();
|
|
78
78
|
|
|
79
79
|
const workspaceStale: StalenessIssue[] = [];
|
|
80
|
-
for (const pkg of
|
|
80
|
+
for (const pkg of getPublishPackages()) {
|
|
81
81
|
const issue = checkStaleVersion(pkg);
|
|
82
82
|
if (issue) workspaceStale.push(issue);
|
|
83
83
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Type definitions for `celilo publish` — the planner/executor split per
|
|
3
|
-
*
|
|
3
|
+
* openspec/changes/publilo-cli/proposal.md Phase 2.
|
|
4
4
|
*
|
|
5
5
|
* The umbrella `PublishPlan` enumerates everything a publish run intends
|
|
6
6
|
* to do. It's produced by the pure planner (`plan.ts`) and consumed by
|
|
@@ -239,7 +239,7 @@ export interface ModuleItem {
|
|
|
239
239
|
|
|
240
240
|
/**
|
|
241
241
|
* Umbrella plan — the central data structure of Phase 2 of the publish
|
|
242
|
-
* CLI refactor (
|
|
242
|
+
* CLI refactor (openspec/changes/publilo-cli/proposal.md). One plan covers all four phases of
|
|
243
243
|
* a publish run; the executor consumes it top-to-bottom.
|
|
244
244
|
*/
|
|
245
245
|
export interface PublishPlan {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workspace planner tests —
|
|
2
|
+
* Workspace planner tests — openspec/changes/publilo-cli/proposal.md Phase 2 payoff.
|
|
3
3
|
*
|
|
4
4
|
* The planner moved out of the monolithic runPublishPhase precisely so
|
|
5
5
|
* these tests would be possible: no npm subprocesses, no bun publish
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Workspace publish — Phase 1 of `celilo publish`.
|
|
3
3
|
*
|
|
4
|
-
* Planner/executor split per
|
|
4
|
+
* Planner/executor split per openspec/changes/publilo-cli/proposal.md Phase 2:
|
|
5
5
|
*
|
|
6
6
|
* planWorkspace(opts) → WorkspaceItem[] // pure, no mutations
|
|
7
7
|
* executeWorkspace(plan, ...) // mutates package.json,
|
|
@@ -325,7 +325,7 @@ export async function verifyPublishedDeps(
|
|
|
325
325
|
* registry target is configured (a deployed npm-cache-node) AND the
|
|
326
326
|
* package is @celilo/*-scoped, point bun at it via --registry; otherwise
|
|
327
327
|
* publish to the default registry (npmjs — current behavior).
|
|
328
|
-
*
|
|
328
|
+
* openspec/changes/private-npm-registry/proposal.md Phase 3.1 / openspec/changes/publilo-cli/proposal.md decision 10.
|
|
329
329
|
*/
|
|
330
330
|
export function buildPublishArgs(
|
|
331
331
|
item: Pick<WorkspaceItem, 'name' | 'tag'>,
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `celilo registry owner list/show/set` (ce-1ch, SECURE_MODULE_PUBLISH.md D-C/D-E).
|
|
3
|
+
* The admin token is resolved from the local publish_tokens list; registry
|
|
4
|
+
* calls are stubbed so these stay hermetic.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test';
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { closeDb, getDb } from '../../db/client';
|
|
13
|
+
import { modules } from '../../db/schema';
|
|
14
|
+
import {
|
|
15
|
+
handleRegistryOwnerList,
|
|
16
|
+
handleRegistryOwnerSet,
|
|
17
|
+
handleRegistryOwnerShow,
|
|
18
|
+
} from './registry-owner';
|
|
19
|
+
import { handleRegistryTokenAdd } from './registry-token';
|
|
20
|
+
|
|
21
|
+
const NO_FLAGS: Record<string, string | boolean> = {};
|
|
22
|
+
|
|
23
|
+
describe('registry owner commands', () => {
|
|
24
|
+
let dataDir: string;
|
|
25
|
+
let fetchSpy: ReturnType<typeof spyOn>;
|
|
26
|
+
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
dataDir = await mkdtemp(join(tmpdir(), 'celilo-registry-owner-'));
|
|
29
|
+
process.env.CELILO_DATA_DIR = dataDir;
|
|
30
|
+
process.env.CELILO_DB_PATH = join(dataDir, 'celilo.db');
|
|
31
|
+
getDb()
|
|
32
|
+
.insert(modules)
|
|
33
|
+
.values({
|
|
34
|
+
id: 'celilo-registry',
|
|
35
|
+
name: 'Celilo Registry',
|
|
36
|
+
version: '1.0.0',
|
|
37
|
+
sourcePath: '/test',
|
|
38
|
+
manifestData: {},
|
|
39
|
+
})
|
|
40
|
+
.run();
|
|
41
|
+
fetchSpy = spyOn(globalThis, 'fetch');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(async () => {
|
|
45
|
+
fetchSpy.mockRestore();
|
|
46
|
+
closeDb();
|
|
47
|
+
process.env.CELILO_DATA_DIR = undefined;
|
|
48
|
+
process.env.CELILO_DB_PATH = undefined;
|
|
49
|
+
if (existsSync(dataDir)) await rm(dataDir, { recursive: true, force: true });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('list errors when no admin token is configured', async () => {
|
|
53
|
+
const result = await handleRegistryOwnerList(NO_FLAGS);
|
|
54
|
+
expect(result.success).toBe(false);
|
|
55
|
+
if (!result.success) expect(result.error).toContain('admin publish token');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('list resolves the admin token and renders the owner table', async () => {
|
|
59
|
+
// Seed a scoped token first (ignored) then a bare admin token (used).
|
|
60
|
+
await handleRegistryTokenAdd(['cpt_scoped homebridge']);
|
|
61
|
+
await handleRegistryTokenAdd(['admin-tok']);
|
|
62
|
+
fetchSpy.mockResolvedValue(
|
|
63
|
+
new Response(
|
|
64
|
+
JSON.stringify({
|
|
65
|
+
owners: [
|
|
66
|
+
{
|
|
67
|
+
moduleName: 'homebridge',
|
|
68
|
+
ownerSub: 'alice',
|
|
69
|
+
claimedAt: 'T',
|
|
70
|
+
sourceGroup: 'celilo-authors',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
}),
|
|
74
|
+
{ status: 200 },
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const result = await handleRegistryOwnerList(NO_FLAGS);
|
|
79
|
+
expect(result.success).toBe(true);
|
|
80
|
+
const [, init] = fetchSpy.mock.calls[0] as [string, RequestInit];
|
|
81
|
+
// The bare admin token is used, not the scoped one.
|
|
82
|
+
expect((init.headers as Record<string, string>).Authorization).toBe('admin-tok');
|
|
83
|
+
if (result.success) expect(result.message).toContain('homebridge');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('list reports an empty table cleanly', async () => {
|
|
87
|
+
await handleRegistryTokenAdd(['admin-tok']);
|
|
88
|
+
fetchSpy.mockResolvedValue(new Response(JSON.stringify({ owners: [] }), { status: 200 }));
|
|
89
|
+
const result = await handleRegistryOwnerList(NO_FLAGS);
|
|
90
|
+
expect(result.success).toBe(true);
|
|
91
|
+
if (result.success) expect(result.message).toContain('No module ownerships');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('show requires a module argument', async () => {
|
|
95
|
+
const result = await handleRegistryOwnerShow([], NO_FLAGS);
|
|
96
|
+
expect(result.success).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('show reports an unclaimed name', async () => {
|
|
100
|
+
await handleRegistryTokenAdd(['admin-tok']);
|
|
101
|
+
fetchSpy.mockResolvedValue(new Response(null, { status: 404 }));
|
|
102
|
+
const result = await handleRegistryOwnerShow(['never-claimed'], NO_FLAGS);
|
|
103
|
+
expect(result.success).toBe(true);
|
|
104
|
+
if (result.success) expect(result.message).toContain('unclaimed');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('show renders the owner of a claimed name', async () => {
|
|
108
|
+
await handleRegistryTokenAdd(['admin-tok']);
|
|
109
|
+
fetchSpy.mockResolvedValue(
|
|
110
|
+
new Response(
|
|
111
|
+
JSON.stringify({
|
|
112
|
+
owner: {
|
|
113
|
+
moduleName: 'caddy',
|
|
114
|
+
ownerSub: 'bob',
|
|
115
|
+
claimedAt: 'T',
|
|
116
|
+
sourceGroup: 'celilo-admins',
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
{ status: 200 },
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
const result = await handleRegistryOwnerShow(['caddy'], NO_FLAGS);
|
|
123
|
+
expect(result.success).toBe(true);
|
|
124
|
+
if (result.success) expect(result.message).toContain('bob');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('set requires both module and owner-sub', async () => {
|
|
128
|
+
expect((await handleRegistryOwnerSet(['homebridge'], NO_FLAGS)).success).toBe(false);
|
|
129
|
+
expect((await handleRegistryOwnerSet([], NO_FLAGS)).success).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('set POSTs the reassignment with the admin token', async () => {
|
|
133
|
+
await handleRegistryTokenAdd(['admin-tok']);
|
|
134
|
+
fetchSpy.mockResolvedValue(
|
|
135
|
+
new Response(
|
|
136
|
+
JSON.stringify({
|
|
137
|
+
ok: true,
|
|
138
|
+
owner: {
|
|
139
|
+
moduleName: 'homebridge',
|
|
140
|
+
ownerSub: 'carol',
|
|
141
|
+
claimedAt: 'T',
|
|
142
|
+
sourceGroup: 'admin-reassign',
|
|
143
|
+
},
|
|
144
|
+
}),
|
|
145
|
+
{ status: 200 },
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
const result = await handleRegistryOwnerSet(['homebridge', 'carol'], NO_FLAGS);
|
|
149
|
+
expect(result.success).toBe(true);
|
|
150
|
+
const [url, init] = fetchSpy.mock.calls[0] as [string, RequestInit];
|
|
151
|
+
expect(url).toContain('/api/v1/modules/owners/homebridge');
|
|
152
|
+
expect(init.method).toBe('POST');
|
|
153
|
+
expect((init.headers as Record<string, string>).Authorization).toBe('admin-tok');
|
|
154
|
+
if (result.success) expect(result.message).toContain('carol');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('surfaces a registry error detail', async () => {
|
|
158
|
+
await handleRegistryTokenAdd(['admin-tok']);
|
|
159
|
+
fetchSpy.mockResolvedValue(
|
|
160
|
+
new Response(JSON.stringify({ errors: [{ detail: 'Unauthorized' }] }), { status: 401 }),
|
|
161
|
+
);
|
|
162
|
+
const result = await handleRegistryOwnerList(NO_FLAGS);
|
|
163
|
+
expect(result.success).toBe(false);
|
|
164
|
+
if (!result.success) expect(result.error).toContain('Unauthorized');
|
|
165
|
+
});
|
|
166
|
+
});
|