@celilo/cli 0.1.8 → 0.2.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/package.json
CHANGED
|
@@ -58,8 +58,16 @@ export const WELL_KNOWN_CAPABILITIES: Record<string, WellKnownCapability> = {
|
|
|
58
58
|
required_zone: 'dmz',
|
|
59
59
|
zone_enforced: false,
|
|
60
60
|
data_schema: {
|
|
61
|
+
// The capability contract no longer carries a `primary_domain`
|
|
62
|
+
// convenience field. Consumers that want a default index into
|
|
63
|
+
// `domains[]` themselves; consumers that need to commit to a
|
|
64
|
+
// specific domain (lunacycle, authentik, knot-unbound-internal,
|
|
65
|
+
// technitium, etc.) declare an explicit user-set `domain`
|
|
66
|
+
// variable in their own manifest. The implicit primary-domain
|
|
67
|
+
// hand-off too easily gave a module the household's first
|
|
68
|
+
// registered domain without anyone deciding to.
|
|
61
69
|
provider: 'namecheap',
|
|
62
|
-
|
|
70
|
+
domains: '$self:domains',
|
|
63
71
|
supports: ['dynamic_dns_a_record'],
|
|
64
72
|
},
|
|
65
73
|
},
|
|
@@ -91,9 +99,9 @@ export const WELL_KNOWN_CAPABILITIES: Record<string, WellKnownCapability> = {
|
|
|
91
99
|
* Note: `oidc.issuer_url` is no longer derived here. Per the firm rule in
|
|
92
100
|
* design/TECHNICAL_DESIGN_MANIFEST_V2.md D9, well-known capability data
|
|
93
101
|
* templates must not cross-reference other capabilities. The IDP-providing
|
|
94
|
-
* module
|
|
95
|
-
*
|
|
96
|
-
*
|
|
102
|
+
* module declares an explicit user-set `domain` field in its own manifest,
|
|
103
|
+
* derives `auth_url` from `$self:domain`, and exposes it through
|
|
104
|
+
* `provides.capabilities[].data`.
|
|
97
105
|
*/
|
|
98
106
|
auth: {
|
|
99
107
|
canonical_hostname: 'auth',
|
|
@@ -22,6 +22,16 @@ import { cleanupTempDir, extractPackage } from '../../module/packaging/extract';
|
|
|
22
22
|
import { log } from '../prompts';
|
|
23
23
|
import type { CommandResult } from '../types';
|
|
24
24
|
|
|
25
|
+
type UpgradeOutcome =
|
|
26
|
+
| { status: 'success'; moduleId: string }
|
|
27
|
+
| { status: 'failed'; moduleId: string; error: string }
|
|
28
|
+
// `skipped` means the path expanded from a glob but isn't an
|
|
29
|
+
// upgradable target — either no manifest at all (probably a non-
|
|
30
|
+
// module sibling like `modules/archive/`) or a real module that
|
|
31
|
+
// isn't installed in this celilo. Treated as a soft pass so
|
|
32
|
+
// `celilo module update modules/*` does what users expect.
|
|
33
|
+
| { status: 'skipped'; moduleId: string; reason: string };
|
|
34
|
+
|
|
25
35
|
/**
|
|
26
36
|
* Upgrade a single module from a source path
|
|
27
37
|
*/
|
|
@@ -29,11 +39,15 @@ async function upgradeOne(
|
|
|
29
39
|
sourcePath: string,
|
|
30
40
|
db: ReturnType<typeof getDb>,
|
|
31
41
|
flags: Record<string, string | boolean> = {},
|
|
32
|
-
): Promise<
|
|
42
|
+
): Promise<UpgradeOutcome> {
|
|
33
43
|
const originalCwd = process.env.CELILO_ORIGINAL_CWD || process.cwd();
|
|
34
44
|
const importPath = resolve(originalCwd, sourcePath);
|
|
35
45
|
if (!existsSync(importPath)) {
|
|
36
|
-
return {
|
|
46
|
+
return {
|
|
47
|
+
status: 'failed',
|
|
48
|
+
moduleId: sourcePath,
|
|
49
|
+
error: `Source path not found: ${importPath}`,
|
|
50
|
+
};
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
// Handle .netapp packages: extract to temp dir
|
|
@@ -44,8 +58,8 @@ async function upgradeOne(
|
|
|
44
58
|
const extractResult = await extractPackage(importPath);
|
|
45
59
|
if (!extractResult.success || !extractResult.tempDir) {
|
|
46
60
|
return {
|
|
61
|
+
status: 'failed',
|
|
47
62
|
moduleId: sourcePath,
|
|
48
|
-
success: false,
|
|
49
63
|
error: extractResult.error || 'Failed to extract package',
|
|
50
64
|
};
|
|
51
65
|
}
|
|
@@ -59,8 +73,8 @@ async function upgradeOne(
|
|
|
59
73
|
if (!verifyResult.success) {
|
|
60
74
|
await cleanupTempDir(tempDir);
|
|
61
75
|
return {
|
|
76
|
+
status: 'failed',
|
|
62
77
|
moduleId: sourcePath,
|
|
63
|
-
success: false,
|
|
64
78
|
error: verifyResult.error || 'Package verification failed',
|
|
65
79
|
};
|
|
66
80
|
}
|
|
@@ -72,10 +86,13 @@ async function upgradeOne(
|
|
|
72
86
|
const manifestPath = join(actualPath, 'manifest.yml');
|
|
73
87
|
if (!existsSync(manifestPath)) {
|
|
74
88
|
if (tempDir) await cleanupTempDir(tempDir);
|
|
89
|
+
// No manifest means the path isn't a module directory at all —
|
|
90
|
+
// a likely outcome of `module update modules/*` matching a
|
|
91
|
+
// non-module sibling. Skip silently rather than fail the batch.
|
|
75
92
|
return {
|
|
93
|
+
status: 'skipped',
|
|
76
94
|
moduleId: sourcePath,
|
|
77
|
-
|
|
78
|
-
error: `No manifest.yml found at ${actualPath}`,
|
|
95
|
+
reason: 'not a module directory (no manifest.yml)',
|
|
79
96
|
};
|
|
80
97
|
}
|
|
81
98
|
|
|
@@ -87,17 +104,22 @@ async function upgradeOne(
|
|
|
87
104
|
} catch (err) {
|
|
88
105
|
if (tempDir) await cleanupTempDir(tempDir);
|
|
89
106
|
const msg = err instanceof Error ? err.message : String(err);
|
|
90
|
-
return {
|
|
107
|
+
return { status: 'failed', moduleId: sourcePath, error: `Invalid manifest: ${msg}` };
|
|
91
108
|
}
|
|
92
109
|
|
|
93
110
|
const moduleId = newManifest.id;
|
|
94
111
|
|
|
95
112
|
const module = db.select().from(modules).where(eq(modules.id, moduleId)).get();
|
|
96
113
|
if (!module) {
|
|
114
|
+
if (tempDir) await cleanupTempDir(tempDir);
|
|
115
|
+
// Module isn't installed in this celilo. Don't fail the batch —
|
|
116
|
+
// `module update modules/*` should keep going for everything
|
|
117
|
+
// that IS installed. Caller surfaces the skip count so the user
|
|
118
|
+
// sees what was passed over.
|
|
97
119
|
return {
|
|
120
|
+
status: 'skipped',
|
|
98
121
|
moduleId,
|
|
99
|
-
|
|
100
|
-
error: `Module '${moduleId}' is not installed. Use 'celilo module import ${sourcePath}' first.`,
|
|
122
|
+
reason: `not installed (run 'celilo module import ${sourcePath}' to add)`,
|
|
101
123
|
};
|
|
102
124
|
}
|
|
103
125
|
|
|
@@ -141,7 +163,7 @@ async function upgradeOne(
|
|
|
141
163
|
}
|
|
142
164
|
|
|
143
165
|
log.success(`Upgraded ${moduleId} (v${oldManifest.version} → v${newManifest.version})`);
|
|
144
|
-
return {
|
|
166
|
+
return { status: 'success', moduleId };
|
|
145
167
|
}
|
|
146
168
|
|
|
147
169
|
/**
|
|
@@ -162,31 +184,60 @@ export async function handleModuleUpgrade(
|
|
|
162
184
|
}
|
|
163
185
|
|
|
164
186
|
const db = getDb();
|
|
165
|
-
const results:
|
|
187
|
+
const results: UpgradeOutcome[] = [];
|
|
166
188
|
|
|
167
189
|
for (const path of args) {
|
|
168
190
|
const result = await upgradeOne(path, db, flags);
|
|
169
191
|
results.push(result);
|
|
170
192
|
}
|
|
171
193
|
|
|
172
|
-
const succeeded = results.filter(
|
|
173
|
-
|
|
194
|
+
const succeeded = results.filter(
|
|
195
|
+
(r): r is Extract<UpgradeOutcome, { status: 'success' }> => r.status === 'success',
|
|
196
|
+
);
|
|
197
|
+
const failed = results.filter(
|
|
198
|
+
(r): r is Extract<UpgradeOutcome, { status: 'failed' }> => r.status === 'failed',
|
|
199
|
+
);
|
|
200
|
+
const skipped = results.filter(
|
|
201
|
+
(r): r is Extract<UpgradeOutcome, { status: 'skipped' }> => r.status === 'skipped',
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Skips that fall under a wildcard expansion (e.g. modules/* picking
|
|
205
|
+
// up `modules/archive/`) shouldn't even be mentioned — they're not
|
|
206
|
+
// signal. Skips for "module not installed" ARE signal because the
|
|
207
|
+
// user explicitly named the path; surface those.
|
|
208
|
+
const meaningfulSkips = skipped.filter((r) => !r.reason.startsWith('not a module directory'));
|
|
174
209
|
|
|
175
210
|
if (failed.length > 0) {
|
|
176
211
|
const errors = failed.map((r) => ` ${r.moduleId}: ${r.error}`).join('\n');
|
|
212
|
+
const parts: string[] = [];
|
|
177
213
|
if (succeeded.length > 0) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
214
|
+
parts.push(
|
|
215
|
+
`Upgraded ${succeeded.length} module(s): ${succeeded.map((r) => r.moduleId).join(', ')}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (meaningfulSkips.length > 0) {
|
|
219
|
+
const skipLines = meaningfulSkips.map((r) => ` ${r.moduleId}: ${r.reason}`).join('\n');
|
|
220
|
+
parts.push(`Skipped ${meaningfulSkips.length}:\n${skipLines}`);
|
|
183
221
|
}
|
|
184
|
-
|
|
222
|
+
parts.push(`Failed ${failed.length}:\n${errors}`);
|
|
223
|
+
return { success: false, error: parts.join('\n\n') };
|
|
185
224
|
}
|
|
186
225
|
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
226
|
+
const lines: string[] = [];
|
|
227
|
+
if (succeeded.length > 0) {
|
|
228
|
+
lines.push(
|
|
229
|
+
`Updated ${succeeded.length} module(s): ${succeeded.map((r) => r.moduleId).join(', ')}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (meaningfulSkips.length > 0) {
|
|
233
|
+
const skipLines = meaningfulSkips.map((r) => ` ${r.moduleId}: ${r.reason}`).join('\n');
|
|
234
|
+
lines.push(`Skipped ${meaningfulSkips.length}:\n${skipLines}`);
|
|
235
|
+
}
|
|
236
|
+
if (lines.length === 0) {
|
|
237
|
+
// Every arg was a non-module sibling — odd but not a failure.
|
|
238
|
+
lines.push(
|
|
239
|
+
`No modules to update (${results.length} path(s) skipped — none had a manifest.yml)`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return { success: true, message: lines.join('\n\n') };
|
|
192
243
|
}
|
|
@@ -519,7 +519,12 @@ async function buildFirewallChain(
|
|
|
519
519
|
|
|
520
520
|
// Apply rules by default. Set CELILO_FIREWALL_DRY_RUN=1 to preview without applying.
|
|
521
521
|
const dryRun = process.env.CELILO_FIREWALL_DRY_RUN === '1';
|
|
522
|
-
|
|
522
|
+
// Third arg (logger) lets iptables route its internal status lines
|
|
523
|
+
// ("[iptables] Rule already exists…", etc.) through the parent
|
|
524
|
+
// celilo's ProgressDisplay instead of dumping to stderr. Older
|
|
525
|
+
// iptables modules ignore it — the factory's signature is
|
|
526
|
+
// backward-compatible.
|
|
527
|
+
const downstreamFirewall = provFactory({ firewallIp, natIp, dryRun }, currentUpstream, logger);
|
|
523
528
|
|
|
524
529
|
debugLog(`firewall chain: wired ${provider.moduleId} → ${hasExternal.moduleId}`);
|
|
525
530
|
// Wrap each downstream layer with auto-logging.
|
|
@@ -77,13 +77,11 @@ const FRAMEWORK_OWNED_PATHS = new Set(['celilo/types.d.ts']);
|
|
|
77
77
|
*
|
|
78
78
|
* `node_modules` exclusion is path-aware: regular npm dependencies are
|
|
79
79
|
* skipped (size + bun re-installs them at module-import time), but the
|
|
80
|
-
* framework's own `@celilo
|
|
81
|
-
* capabilities API the module was authored against, so a
|
|
82
|
-
* hooks aren't silently broken by a runtime that ships a
|
|
83
|
-
* version of `@celilo/capabilities` than the one on npm.
|
|
80
|
+
* framework's own `@celilo/capabilities` package stays in. That
|
|
81
|
+
* captures the capabilities API the module was authored against, so a
|
|
82
|
+
* module's hooks aren't silently broken by a runtime that ships a
|
|
83
|
+
* different version of `@celilo/capabilities` than the one on npm.
|
|
84
84
|
*/
|
|
85
|
-
const BUNDLED_CELILO_PACKAGES = new Set(['capabilities', 'cli-display']);
|
|
86
|
-
|
|
87
85
|
function shouldExclude(filePath: string): boolean {
|
|
88
86
|
if (FRAMEWORK_OWNED_PATHS.has(filePath)) return true;
|
|
89
87
|
|
|
@@ -95,18 +93,15 @@ function shouldExclude(filePath: string): boolean {
|
|
|
95
93
|
|
|
96
94
|
const nmIdx = segments.indexOf('node_modules');
|
|
97
95
|
if (nmIdx >= 0) {
|
|
98
|
-
// `node_modules` itself: descend so we can pick out @celilo
|
|
99
|
-
//
|
|
100
|
-
// authored against
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
// else is regular npm cruft we'd just re-install on the target
|
|
104
|
-
// (or test-only deps inside packages like `@celilo/e2e` we don't
|
|
105
|
-
// want to drag in).
|
|
96
|
+
// `node_modules` itself: descend so we can pick out @celilo/capabilities.
|
|
97
|
+
// The capabilities scope is the only thing we ship — it's the framework
|
|
98
|
+
// SDK the module was authored against. Everything else is regular npm
|
|
99
|
+
// cruft we'd just re-install on the target (or test-only deps inside
|
|
100
|
+
// packages like `@celilo/e2e` we don't want to drag in).
|
|
106
101
|
if (nmIdx + 1 >= segments.length) return false; // node_modules dir itself
|
|
107
102
|
if (segments[nmIdx + 1] !== '@celilo') return true;
|
|
108
103
|
if (nmIdx + 2 >= segments.length) return false; // node_modules/@celilo dir itself
|
|
109
|
-
return
|
|
104
|
+
return segments[nmIdx + 2] !== 'capabilities';
|
|
110
105
|
}
|
|
111
106
|
|
|
112
107
|
const name = basename(filePath);
|