@celilo/cli 0.9.1 → 0.11.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/CELILO_CORE_MODULES.md +1 -1
- package/CELILO_SUBSYSTEMS.md +16 -0
- package/drizzle/0014_api_principals.sql +10 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +14 -6
- package/src/api/protocol.test.ts +76 -0
- package/src/api/remote-client.test.ts +91 -0
- package/src/api/serve.ts +159 -0
- package/src/cli/command-tree-parser.ts +3 -1
- package/src/cli/commands/api.ts +194 -0
- package/src/cli/commands/apt-upgrade.test.ts +33 -0
- package/src/cli/commands/apt-upgrade.ts +63 -0
- package/src/cli/commands/commands-json.ts +29 -0
- package/src/cli/commands/completion.ts +1 -1
- package/src/cli/commands/module-list.ts +16 -2
- package/src/cli/commands/publish/helpers.ts +18 -0
- package/src/cli/commands/publish/types.ts +6 -8
- package/src/cli/commands/publish/workspace.test.ts +44 -7
- package/src/cli/commands/publish/workspace.ts +40 -164
- package/src/cli/commands/service-list.ts +15 -2
- package/src/cli/completion.ts +24 -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 +109 -2
- package/src/cli/parser.test.ts +13 -0
- package/src/cli/parser.ts +12 -3
- package/src/db/schema.ts +30 -0
- package/src/hooks/capability-loader.test.ts +77 -0
- package/src/hooks/capability-loader.ts +56 -0
- package/src/services/api-access.test.ts +138 -0
- package/src/services/api-access.ts +154 -0
- package/src/services/remote-responder.test.ts +78 -0
- package/src/services/remote-responder.ts +89 -0
- package/src/cli/command-registry.ts +0 -1443
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `celilo apt-upgrade` — upgrade the deb-installed celilo packages and apply
|
|
3
|
+
* pending DB migrations. The management server (celilo-bootstrap) installs
|
|
4
|
+
* celilo via apt, so keeping it current means the apt chain, not `bun update -g`
|
|
5
|
+
* (that path is `system update`'s self-update, for npm-global installs).
|
|
6
|
+
*
|
|
7
|
+
* Steps (ISS-0100 — the postinst does NOT auto-apply migrations):
|
|
8
|
+
* 1. apt-get update
|
|
9
|
+
* 2. apt-get -y --only-upgrade install celilo celilo-bootstrap
|
|
10
|
+
* 3. a FRESH `celilo system migrate` — spawned as the just-installed binary so
|
|
11
|
+
* the new version's migrations run, not the ones loaded in this process.
|
|
12
|
+
*
|
|
13
|
+
* This is the RW target behind the MCP's `celilo_apt_upgrade` tool. It runs as
|
|
14
|
+
* the celilo user (via api-serve); the apt steps sudo to root, gated by the
|
|
15
|
+
* scoped /etc/sudoers.d/celilo-apt-upgrade grant that celilo-bootstrap ships.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import type { CommandResult } from '../types';
|
|
20
|
+
|
|
21
|
+
/** Wrapper the deb installs; the fresh migrate step runs the upgraded binary. */
|
|
22
|
+
const CELILO_BIN = '/usr/local/bin/celilo';
|
|
23
|
+
|
|
24
|
+
/** One command to run in the chain — argv plus a human label for output. */
|
|
25
|
+
interface Step {
|
|
26
|
+
label: string;
|
|
27
|
+
argv: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const STEPS: Step[] = [
|
|
31
|
+
{ label: 'apt-get update', argv: ['sudo', 'apt-get', 'update'] },
|
|
32
|
+
{
|
|
33
|
+
label: 'apt-get upgrade celilo, celilo-bootstrap',
|
|
34
|
+
argv: ['sudo', 'apt-get', '-y', '--only-upgrade', 'install', 'celilo', 'celilo-bootstrap'],
|
|
35
|
+
},
|
|
36
|
+
{ label: 'apply DB migrations', argv: [CELILO_BIN, 'system', 'migrate'] },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** Run one argv, inheriting stdio so its output streams through api-serve. */
|
|
40
|
+
export type StepRunner = (argv: string[]) => { status: number | null };
|
|
41
|
+
|
|
42
|
+
const defaultRunner: StepRunner = (argv) => spawnSync(argv[0], argv.slice(1), { stdio: 'inherit' });
|
|
43
|
+
|
|
44
|
+
export async function handleAptUpgrade(
|
|
45
|
+
_args: string[],
|
|
46
|
+
_flags: Record<string, string | boolean>,
|
|
47
|
+
runStep: StepRunner = defaultRunner,
|
|
48
|
+
): Promise<CommandResult> {
|
|
49
|
+
for (const step of STEPS) {
|
|
50
|
+
process.stdout.write(`\n▸ ${step.label}\n`);
|
|
51
|
+
const { status } = runStep(step.argv);
|
|
52
|
+
if (status !== 0) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
error: `apt-upgrade failed at "${step.label}" (exit ${status ?? 'signal'}). Nothing further was run.`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
success: true,
|
|
61
|
+
message: 'celilo apt packages upgraded and migrations applied.',
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commands Command
|
|
3
|
+
*
|
|
4
|
+
* Serializes the CLI command registry (`COMMANDS`) as JSON so an external
|
|
5
|
+
* consumer — `@celilo/mcp` above all — can fetch the *live* surface of whatever
|
|
6
|
+
* celilo version this server runs and generate its tool set from it, rather than
|
|
7
|
+
* compiling a copy that drifts (design D3). The registry is already structured
|
|
8
|
+
* data; this just prints it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { COMMANDS } from '@celilo/core';
|
|
12
|
+
import type { CommandResult } from '../types';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handle `celilo commands [--json]`.
|
|
16
|
+
*
|
|
17
|
+
* The only useful output is the JSON tree, so we emit it whether or not `--json`
|
|
18
|
+
* is passed; the flag exists to make the intent explicit and completable.
|
|
19
|
+
*/
|
|
20
|
+
export async function handleCommands(
|
|
21
|
+
_args: string[],
|
|
22
|
+
_flags: Record<string, boolean | string> = {},
|
|
23
|
+
): Promise<CommandResult> {
|
|
24
|
+
return {
|
|
25
|
+
success: true,
|
|
26
|
+
message: JSON.stringify(COMMANDS),
|
|
27
|
+
rawOutput: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Generate shell completion scripts for bash/zsh
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { COMMANDS } from '
|
|
6
|
+
import { COMMANDS } from '@celilo/core';
|
|
7
7
|
import { generateBashCompletion, generateFishCompletion } from '../completion';
|
|
8
8
|
import { generateRichZshCompletion } from '../generate-zsh-completion';
|
|
9
9
|
import { celiloIntro } from '../prompts';
|
|
@@ -1,20 +1,34 @@
|
|
|
1
1
|
import { getDb } from '../../db/client';
|
|
2
2
|
import { modules } from '../../db/schema';
|
|
3
|
+
import { hasFlag } from '../parser';
|
|
3
4
|
import type { CommandResult } from '../types';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Handle module list command
|
|
7
8
|
*
|
|
8
|
-
* Usage: celilo module list
|
|
9
|
+
* Usage: celilo module list [--json]
|
|
9
10
|
*
|
|
10
11
|
* @returns Command result
|
|
11
12
|
*/
|
|
12
|
-
export async function handleModuleList(
|
|
13
|
+
export async function handleModuleList(
|
|
14
|
+
flags: Record<string, string | boolean> = {},
|
|
15
|
+
): Promise<CommandResult> {
|
|
13
16
|
const db = getDb();
|
|
14
17
|
|
|
15
18
|
// Query all modules
|
|
16
19
|
const moduleRows = db.select().from(modules).all();
|
|
17
20
|
|
|
21
|
+
// Stable machine-readable roster — the backbone the MCP composite
|
|
22
|
+
// troubleshooting tools correlate audit findings against (ce-77i.5).
|
|
23
|
+
if (hasFlag(flags, 'json')) {
|
|
24
|
+
return {
|
|
25
|
+
success: true,
|
|
26
|
+
message: JSON.stringify({ modules: moduleRows }, null, 2),
|
|
27
|
+
rawOutput: true,
|
|
28
|
+
data: moduleRows,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
if (moduleRows.length === 0) {
|
|
19
33
|
return {
|
|
20
34
|
success: true,
|
|
@@ -125,6 +125,24 @@ export function readExternalProjectPaths(): string[] {
|
|
|
125
125
|
return [];
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Read `NPM_PUBLISH_TARGET` from .env — the registry URL `celilo publish`
|
|
130
|
+
* points `bun publish --registry` at for @celilo/* packages (a deployed
|
|
131
|
+
* npm-cache-node). Unset → publish to npmjs (current behavior). See
|
|
132
|
+
* v2/NPM_CACHE_NODE.md Phase 3.1 / v2/PUBLILO_CLI.md decision 10.
|
|
133
|
+
*/
|
|
134
|
+
export function readNpmPublishTarget(): string | null {
|
|
135
|
+
if (!existsSync(ENV_FILE)) return null;
|
|
136
|
+
const content = readFileSync(ENV_FILE, 'utf-8');
|
|
137
|
+
for (const line of content.split('\n')) {
|
|
138
|
+
const m = line.match(/^\s*NPM_PUBLISH_TARGET\s*=\s*(.+?)\s*$/);
|
|
139
|
+
if (!m) continue;
|
|
140
|
+
const raw = m[1].replace(/^["']|["']$/g, '').trim();
|
|
141
|
+
return raw || null;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
128
146
|
/**
|
|
129
147
|
* Recursively find every package.json under a root, skipping
|
|
130
148
|
* node_modules and common build-output dirs (so we don't try to rewrite
|
|
@@ -139,15 +139,13 @@ export interface RewriteOptions {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
|
-
* Per-package pre-publish work. Currently only `@celilo/e2e` triggers
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
142
|
+
* Per-package pre-publish work. Currently only `@celilo/e2e` triggers this
|
|
143
|
+
* (it bundles the npm-compat registry server source inside its tarball).
|
|
144
|
+
* Sim-content caches (website/npm) and standard-module netapps are NOT
|
|
145
|
+
* bundled at publish — they're fetched from the public celilo sources at
|
|
146
|
+
* `cele2e build-infra` time instead (ce-qwz Decisions 2B + 3, ce-i2i).
|
|
146
147
|
*/
|
|
147
|
-
export type PrePublishHookKind =
|
|
148
|
-
| 'registryServerBundle'
|
|
149
|
-
| 'rebuildE2eNetapps'
|
|
150
|
-
| 'stageE2ePublishCaches';
|
|
148
|
+
export type PrePublishHookKind = 'registryServerBundle';
|
|
151
149
|
|
|
152
150
|
/**
|
|
153
151
|
* Single workspace package planned to publish (or explicitly skip) in
|
|
@@ -57,6 +57,7 @@ mock.module('./helpers', () => ({
|
|
|
57
57
|
},
|
|
58
58
|
listModuleDirs: () => [],
|
|
59
59
|
readExternalProjectPaths: () => [],
|
|
60
|
+
readNpmPublishTarget: () => null,
|
|
60
61
|
findPackageJsons: () => [],
|
|
61
62
|
bareVersion: (s: string) => s.replace(/^[\s^~=><]+/, '').trim(),
|
|
62
63
|
withOperator: (oldSpec: string, newVersion: string) => {
|
|
@@ -74,6 +75,10 @@ mock.module('./alpha', () => ({
|
|
|
74
75
|
return { name: spec.slice(0, i), version: spec.slice(i + 1) };
|
|
75
76
|
},
|
|
76
77
|
stripAlphaSuffix: (v: string) => v.replace(/-alpha\.\d+$/, ''),
|
|
78
|
+
prereleaseDistTag: (v: string) => {
|
|
79
|
+
const dash = v.indexOf('-');
|
|
80
|
+
return dash === -1 ? undefined : v.slice(dash + 1).split('.')[0] || undefined;
|
|
81
|
+
},
|
|
77
82
|
isAlphaVersion: (v: string) => /-alpha\.\d+$/.test(v),
|
|
78
83
|
nextAlphaNumber: (name: string, semverCore: string) =>
|
|
79
84
|
nextAlphaResponses[`${name}@${semverCore}`] ?? 0,
|
|
@@ -84,7 +89,7 @@ mock.module('./alpha', () => ({
|
|
|
84
89
|
decideAlphaSkip: () => ({ skip: false }),
|
|
85
90
|
}));
|
|
86
91
|
|
|
87
|
-
const { planWorkspace } = await import('./workspace');
|
|
92
|
+
const { planWorkspace, buildPublishArgs } = await import('./workspace');
|
|
88
93
|
|
|
89
94
|
function buildBaseMap(): Map<string, string> {
|
|
90
95
|
const m = new Map<string, string>();
|
|
@@ -131,7 +136,7 @@ describe('planWorkspace', () => {
|
|
|
131
136
|
expect(cli?.skipReason).toBeUndefined();
|
|
132
137
|
});
|
|
133
138
|
|
|
134
|
-
test('e2e gets the registry-bundle
|
|
139
|
+
test('e2e gets only the registry-bundle hook (caches/netapps fetched at build-infra, not bundled)', () => {
|
|
135
140
|
publishedSet = new Set();
|
|
136
141
|
const result = planWorkspace({
|
|
137
142
|
mode: { kind: 'normal' },
|
|
@@ -141,11 +146,7 @@ describe('planWorkspace', () => {
|
|
|
141
146
|
});
|
|
142
147
|
|
|
143
148
|
const e2e = result.items.find((i) => i.pkg === 'packages/e2e');
|
|
144
|
-
expect(e2e?.hooks).toEqual([
|
|
145
|
-
'registryServerBundle',
|
|
146
|
-
'rebuildE2eNetapps',
|
|
147
|
-
'stageE2ePublishCaches',
|
|
148
|
-
]);
|
|
149
|
+
expect(e2e?.hooks).toEqual(['registryServerBundle']);
|
|
149
150
|
const cli = result.items.find((i) => i.pkg === 'apps/celilo');
|
|
150
151
|
expect(cli?.hooks).toEqual([]);
|
|
151
152
|
});
|
|
@@ -302,6 +303,42 @@ describe('planWorkspace', () => {
|
|
|
302
303
|
});
|
|
303
304
|
});
|
|
304
305
|
|
|
306
|
+
describe('buildPublishArgs registry target', () => {
|
|
307
|
+
test('no target → default registry (unchanged behavior)', () => {
|
|
308
|
+
expect(buildPublishArgs({ name: '@celilo/cli', tag: undefined }, null)).toEqual([
|
|
309
|
+
'publish',
|
|
310
|
+
'--access',
|
|
311
|
+
'public',
|
|
312
|
+
]);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('target set → --registry appended for @celilo/* package', () => {
|
|
316
|
+
expect(
|
|
317
|
+
buildPublishArgs({ name: '@celilo/cli', tag: undefined }, 'https://npm.example.test/'),
|
|
318
|
+
).toEqual(['publish', '--access', 'public', '--registry', 'https://npm.example.test/']);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('tag and registry both present', () => {
|
|
322
|
+
expect(
|
|
323
|
+
buildPublishArgs({ name: '@celilo/cli', tag: 'alpha' }, 'https://npm.example.test/'),
|
|
324
|
+
).toEqual([
|
|
325
|
+
'publish',
|
|
326
|
+
'--access',
|
|
327
|
+
'public',
|
|
328
|
+
'--tag',
|
|
329
|
+
'alpha',
|
|
330
|
+
'--registry',
|
|
331
|
+
'https://npm.example.test/',
|
|
332
|
+
]);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test('non-@celilo package never gets the private registry', () => {
|
|
336
|
+
expect(
|
|
337
|
+
buildPublishArgs({ name: 'some-other-pkg', tag: undefined }, 'https://npm.example.test/'),
|
|
338
|
+
).toEqual(['publish', '--access', 'public']);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
305
342
|
describe('dependency order', () => {
|
|
306
343
|
test('preserves PACKAGES order in planned items', () => {
|
|
307
344
|
publishedSet = new Set();
|
|
@@ -17,18 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { spawnSync } from 'node:child_process';
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
existsSync,
|
|
23
|
-
mkdirSync,
|
|
24
|
-
readFileSync,
|
|
25
|
-
readdirSync,
|
|
26
|
-
rmSync,
|
|
27
|
-
statSync,
|
|
28
|
-
unlinkSync,
|
|
29
|
-
writeFileSync,
|
|
30
|
-
} from 'node:fs';
|
|
31
|
-
import { join, relative } from 'node:path';
|
|
20
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
32
22
|
import {
|
|
33
23
|
ALPHA_TAG,
|
|
34
24
|
alphaSkipDecision,
|
|
@@ -36,7 +26,7 @@ import {
|
|
|
36
26
|
prereleaseDistTag,
|
|
37
27
|
stripAlphaSuffix,
|
|
38
28
|
} from './alpha';
|
|
39
|
-
import { REPO_ROOT, isPublished, readPkg } from './helpers';
|
|
29
|
+
import { REPO_ROOT, isPublished, readNpmPublishTarget, readPkg } from './helpers';
|
|
40
30
|
import type {
|
|
41
31
|
PackageJson,
|
|
42
32
|
PublishMode,
|
|
@@ -74,13 +64,15 @@ export interface PlanWorkspaceOutput {
|
|
|
74
64
|
|
|
75
65
|
/**
|
|
76
66
|
* Pre-publish hooks that fire for @celilo/e2e (the only package that
|
|
77
|
-
* needs
|
|
78
|
-
*
|
|
79
|
-
*
|
|
67
|
+
* needs one today): refresh the bundled npm-compat registry-server source.
|
|
68
|
+
* Sim-content caches and standard-module netapps are no longer staged into
|
|
69
|
+
* the tarball at publish — a monorepo-free consumer fetches them from the
|
|
70
|
+
* public celilo sources at `cele2e build-infra` time (ce-qwz Decisions
|
|
71
|
+
* 2B + 3, ce-i2i).
|
|
80
72
|
*/
|
|
81
73
|
function workspaceHooksFor(pkg: string): WorkspaceItem['hooks'] {
|
|
82
74
|
if (pkg !== 'packages/e2e') return [];
|
|
83
|
-
return ['registryServerBundle'
|
|
75
|
+
return ['registryServerBundle'];
|
|
84
76
|
}
|
|
85
77
|
|
|
86
78
|
/**
|
|
@@ -328,147 +320,23 @@ export async function verifyPublishedDeps(
|
|
|
328
320
|
}
|
|
329
321
|
}
|
|
330
322
|
|
|
331
|
-
// ─── Per-package pre-publish hooks ─────────────────────────────────
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Modules excluded from the @celilo/e2e netapps shipment:
|
|
335
|
-
* - celilo-registry: bundles the bun-compiled registry server
|
|
336
|
-
* binaries (~76 MB packed). Not used by typical consumer e2e
|
|
337
|
-
* tests; including it would bloat the npm tarball past the SSL
|
|
338
|
-
* transport's reliable window.
|
|
339
|
-
* - archive: not a real module dir.
|
|
340
|
-
*/
|
|
341
|
-
const E2E_NETAPP_EXCLUDES = new Set(['archive', 'celilo-registry']);
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Pre-publish step for @celilo/e2e: package each (non-excluded)
|
|
345
|
-
* module under `<root>/modules/` into `packages/e2e/netapps/`. Replaces
|
|
346
|
-
* whatever was last left there by a local `cele2e build-infra` so the
|
|
347
|
-
* shipped tarball is always built from the current branch, not the
|
|
348
|
-
* publisher's last local development build.
|
|
349
|
-
*/
|
|
350
|
-
export function rebuildE2eNetapps(repoRoot: string): void {
|
|
351
|
-
const modulesDir = join(repoRoot, 'modules');
|
|
352
|
-
const netappsDir = join(repoRoot, 'packages/e2e/netapps');
|
|
353
|
-
const celiloWrapper = join(repoRoot, 'celilo');
|
|
354
|
-
|
|
355
|
-
if (!existsSync(modulesDir)) {
|
|
356
|
-
console.warn(
|
|
357
|
-
`⚠ ${modulesDir} not found — skipping netapp rebuild. The published tarball will reuse whatever's currently in packages/e2e/netapps/.`,
|
|
358
|
-
);
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
if (!existsSync(celiloWrapper)) {
|
|
362
|
-
console.warn(
|
|
363
|
-
`⚠ ${celiloWrapper} not found — skipping netapp rebuild. Same staleness risk as above.`,
|
|
364
|
-
);
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
console.log('Rebuilding packages/e2e/netapps/ from infra/modules/...');
|
|
369
|
-
|
|
370
|
-
for (const existing of readdirSync(netappsDir).filter((f) => f.endsWith('.netapp'))) {
|
|
371
|
-
try {
|
|
372
|
-
unlinkSync(join(netappsDir, existing));
|
|
373
|
-
} catch {
|
|
374
|
-
// Best effort
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const moduleDirs = readdirSync(modulesDir).filter((name) => {
|
|
379
|
-
if (E2E_NETAPP_EXCLUDES.has(name)) return false;
|
|
380
|
-
const dir = join(modulesDir, name);
|
|
381
|
-
try {
|
|
382
|
-
return statSync(dir).isDirectory() && existsSync(join(dir, 'manifest.yml'));
|
|
383
|
-
} catch {
|
|
384
|
-
return false;
|
|
385
|
-
}
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
let okCount = 0;
|
|
389
|
-
const failures: Array<{ name: string; stderr: string }> = [];
|
|
390
|
-
for (const name of moduleDirs) {
|
|
391
|
-
const moduleDir = join(modulesDir, name);
|
|
392
|
-
const out = join(netappsDir, `${name}.netapp`);
|
|
393
|
-
const r = spawnSync(celiloWrapper, ['package', moduleDir, '--output', out], {
|
|
394
|
-
cwd: repoRoot,
|
|
395
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
396
|
-
encoding: 'utf-8',
|
|
397
|
-
});
|
|
398
|
-
if (r.status === 0) {
|
|
399
|
-
okCount++;
|
|
400
|
-
} else {
|
|
401
|
-
failures.push({ name, stderr: (r.stderr ?? '').trim() || `exit ${r.status}` });
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
if (failures.length > 0) {
|
|
406
|
-
console.error(`✗ Failed to package ${failures.length} module(s):`);
|
|
407
|
-
for (const f of failures) {
|
|
408
|
-
console.error(` ${f.name}: ${f.stderr}`);
|
|
409
|
-
}
|
|
410
|
-
console.error(
|
|
411
|
-
'Aborting publish — shipping with missing netapps would silently break consumer e2e tests.',
|
|
412
|
-
);
|
|
413
|
-
process.exit(1);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
console.log(
|
|
417
|
-
`Refreshed ${okCount} netapp(s) in packages/e2e/netapps/ (excluded: ${[...E2E_NETAPP_EXCLUDES].join(', ')}).\n`,
|
|
418
|
-
);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
323
|
/**
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
324
|
+
* Build the `bun publish` argv for a workspace item. When a private
|
|
325
|
+
* registry target is configured (a deployed npm-cache-node) AND the
|
|
326
|
+
* package is @celilo/*-scoped, point bun at it via --registry; otherwise
|
|
327
|
+
* publish to the default registry (npmjs — current behavior).
|
|
328
|
+
* v2/NPM_CACHE_NODE.md Phase 3.1 / v2/PUBLILO_CLI.md decision 10.
|
|
426
329
|
*/
|
|
427
|
-
export function
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
console.error(`✗ ${websiteSrc} not found — cannot stage .celilo-website-cache.`);
|
|
436
|
-
process.exit(1);
|
|
330
|
+
export function buildPublishArgs(
|
|
331
|
+
item: Pick<WorkspaceItem, 'name' | 'tag'>,
|
|
332
|
+
registryTarget: string | null,
|
|
333
|
+
): string[] {
|
|
334
|
+
const args = ['publish', '--access', 'public'];
|
|
335
|
+
if (item.tag) args.push('--tag', item.tag);
|
|
336
|
+
if (registryTarget && item.name.startsWith('@celilo/')) {
|
|
337
|
+
args.push('--registry', registryTarget);
|
|
437
338
|
}
|
|
438
|
-
|
|
439
|
-
console.error(`✗ ${packScript} not found — cannot stage .npm-registry-cache.`);
|
|
440
|
-
process.exit(1);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
console.log('Staging .celilo-website-cache/ from modules/celilo-website/site/...');
|
|
444
|
-
const installResult = spawnSync('bun', ['install'], { cwd: websiteSrc, stdio: 'pipe' });
|
|
445
|
-
if (installResult.status !== 0) {
|
|
446
|
-
console.error('✗ bun install for celilo-website failed:');
|
|
447
|
-
console.error(installResult.stderr?.toString());
|
|
448
|
-
process.exit(1);
|
|
449
|
-
}
|
|
450
|
-
const buildResult = spawnSync('bun', ['run', 'build'], { cwd: websiteSrc, stdio: 'pipe' });
|
|
451
|
-
if (buildResult.status !== 0) {
|
|
452
|
-
console.error('✗ bun run build for celilo-website failed:');
|
|
453
|
-
console.error(buildResult.stderr?.toString());
|
|
454
|
-
process.exit(1);
|
|
455
|
-
}
|
|
456
|
-
rmSync(websiteCache, { recursive: true, force: true });
|
|
457
|
-
mkdirSync(websiteCache, { recursive: true });
|
|
458
|
-
cpSync(join(websiteSrc, 'dist'), websiteCache, { recursive: true });
|
|
459
|
-
console.log(`✓ Staged .celilo-website-cache/ (from ${relative(repoRoot, websiteSrc)}/dist/)\n`);
|
|
460
|
-
|
|
461
|
-
console.log('Staging .npm-registry-cache/ from @celilo/* workspace tarballs...');
|
|
462
|
-
const packResult = spawnSync('bun', ['run', packScript], { cwd: repoRoot, stdio: 'pipe' });
|
|
463
|
-
if (packResult.status !== 0) {
|
|
464
|
-
console.error('✗ pack-celilo-packages.ts failed:');
|
|
465
|
-
console.error(packResult.stderr?.toString());
|
|
466
|
-
process.exit(1);
|
|
467
|
-
}
|
|
468
|
-
const packed = existsSync(npmCache)
|
|
469
|
-
? readdirSync(npmCache).filter((f) => f.endsWith('.tgz'))
|
|
470
|
-
: [];
|
|
471
|
-
console.log(`✓ Staged .npm-registry-cache/ with ${packed.length} workspace tarball(s)\n`);
|
|
339
|
+
return args;
|
|
472
340
|
}
|
|
473
341
|
|
|
474
342
|
// ─── Executor ──────────────────────────────────────────────────────
|
|
@@ -486,6 +354,13 @@ export async function executeWorkspace(input: ExecuteWorkspaceInput): Promise<Pu
|
|
|
486
354
|
const published: PublishResult['published'] = [];
|
|
487
355
|
const skipped: string[] = [];
|
|
488
356
|
|
|
357
|
+
// When an npm-cache-node is configured, @celilo/* tarballs go there
|
|
358
|
+
// (the cache forwards upstream under policy); unset → npmjs.
|
|
359
|
+
const registryTarget = readNpmPublishTarget();
|
|
360
|
+
if (registryTarget) {
|
|
361
|
+
console.log(`\nPublishing @celilo/* to configured registry: ${registryTarget}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
489
364
|
for (const item of items) {
|
|
490
365
|
const { pkg, name, baseVersion, versionToPublish } = item;
|
|
491
366
|
|
|
@@ -511,12 +386,6 @@ export async function executeWorkspace(input: ExecuteWorkspaceInput): Promise<Pu
|
|
|
511
386
|
'Refreshed packages/e2e/registry-server/ bundle from packages/registry-server.\n',
|
|
512
387
|
);
|
|
513
388
|
}
|
|
514
|
-
if (item.hooks.includes('rebuildE2eNetapps')) {
|
|
515
|
-
rebuildE2eNetapps(REPO_ROOT);
|
|
516
|
-
}
|
|
517
|
-
if (item.hooks.includes('stageE2ePublishCaches')) {
|
|
518
|
-
stageE2ePublishCaches(REPO_ROOT);
|
|
519
|
-
}
|
|
520
389
|
|
|
521
390
|
const { original: pkgJsonOriginal, rewrites: workspaceRewrites } = rewriteWorkspaceDeps(
|
|
522
391
|
pkg,
|
|
@@ -548,8 +417,7 @@ export async function executeWorkspace(input: ExecuteWorkspaceInput): Promise<Pu
|
|
|
548
417
|
process.exit(1);
|
|
549
418
|
}
|
|
550
419
|
|
|
551
|
-
const publishArgs =
|
|
552
|
-
if (item.tag) publishArgs.push('--tag', item.tag);
|
|
420
|
+
const publishArgs = buildPublishArgs(item, registryTarget);
|
|
553
421
|
|
|
554
422
|
let publishStatus: number | null = null;
|
|
555
423
|
let publishError: unknown = null;
|
|
@@ -591,7 +459,15 @@ export async function executeWorkspace(input: ExecuteWorkspaceInput): Promise<Pu
|
|
|
591
459
|
}
|
|
592
460
|
|
|
593
461
|
if (workspaceRewrites.length > 0) {
|
|
594
|
-
|
|
462
|
+
// Verify against the same registry we published to — a cache node
|
|
463
|
+
// forwards upstream under policy, so the version may not be on
|
|
464
|
+
// npmjs yet.
|
|
465
|
+
await verifyPublishedDeps(
|
|
466
|
+
name,
|
|
467
|
+
versionToPublish,
|
|
468
|
+
workspaceRewrites,
|
|
469
|
+
registryTarget ?? undefined,
|
|
470
|
+
);
|
|
595
471
|
}
|
|
596
472
|
|
|
597
473
|
// Silence the unused-var warning on baseVersion — we keep the value
|
|
@@ -22,8 +22,6 @@ export async function handleServiceList(
|
|
|
22
22
|
flags: Record<string, boolean | string> = {},
|
|
23
23
|
): Promise<CommandResult> {
|
|
24
24
|
try {
|
|
25
|
-
celiloIntro('Container Services');
|
|
26
|
-
|
|
27
25
|
// Apply zone filter if provided
|
|
28
26
|
const filters: ContainerServiceFilters = {};
|
|
29
27
|
if (flags.zone && typeof flags.zone === 'string') {
|
|
@@ -32,6 +30,21 @@ export async function handleServiceList(
|
|
|
32
30
|
|
|
33
31
|
const services = await listContainerServices(filters);
|
|
34
32
|
|
|
33
|
+
// Machine-readable output (consumed by @celilo/mcp auto-detect). Emit the
|
|
34
|
+
// fields detection needs — provider drives which tool groups surface.
|
|
35
|
+
if (flags.json) {
|
|
36
|
+
const payload = services.map((s) => ({
|
|
37
|
+
serviceId: s.serviceId,
|
|
38
|
+
name: s.name,
|
|
39
|
+
provider: s.providerName,
|
|
40
|
+
zones: s.zones,
|
|
41
|
+
verified: s.verified,
|
|
42
|
+
}));
|
|
43
|
+
return { success: true, message: JSON.stringify(payload), rawOutput: true };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
celiloIntro('Container Services');
|
|
47
|
+
|
|
35
48
|
if (services.length === 0) {
|
|
36
49
|
console.log('No container services configured.\n');
|
|
37
50
|
console.log('Add a service:');
|
package/src/cli/completion.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { eq } from 'drizzle-orm';
|
|
|
7
7
|
import { getDb } from '../db/client';
|
|
8
8
|
import { capabilities, modules } from '../db/schema';
|
|
9
9
|
import type { ModuleManifest } from '../manifest/schema';
|
|
10
|
+
import { listPrincipals } from '../services/api-access';
|
|
10
11
|
import { listBackups } from '../services/backup-metadata';
|
|
11
12
|
import { listBackupStorages } from '../services/backup-storage';
|
|
12
13
|
import { listContainerServices } from '../services/container-service';
|
|
@@ -28,9 +29,12 @@ export async function getCompletions(words: string[], current: number): Promise<
|
|
|
28
29
|
// currentIndex === 0 means we're completing the first word (the command)
|
|
29
30
|
if (currentIndex === 0) {
|
|
30
31
|
const commands = [
|
|
32
|
+
'api',
|
|
33
|
+
'apt-upgrade',
|
|
31
34
|
'audit',
|
|
32
35
|
'backup',
|
|
33
36
|
'capability',
|
|
37
|
+
'commands',
|
|
34
38
|
'dns',
|
|
35
39
|
'completion',
|
|
36
40
|
'events',
|
|
@@ -332,6 +336,26 @@ export async function getCompletions(words: string[], current: number): Promise<
|
|
|
332
336
|
return filterSuggestions(configKeys, args[4] || '');
|
|
333
337
|
}
|
|
334
338
|
|
|
339
|
+
// API subcommands
|
|
340
|
+
if (command === 'api' && currentIndex === 1) {
|
|
341
|
+
const subcommands = ['grant', 'list', 'revoke', 'authorized-keys', 'key'];
|
|
342
|
+
return filterSuggestions(subcommands, args[1] || '');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// API revoke - complete with principal names
|
|
346
|
+
if (command === 'api' && args[1] === 'revoke' && currentIndex === 2) {
|
|
347
|
+
const principals = await listPrincipals();
|
|
348
|
+
return filterSuggestions(
|
|
349
|
+
principals.map((p) => p.name),
|
|
350
|
+
args[2] || '',
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// API key subcommands
|
|
355
|
+
if (command === 'api' && args[1] === 'key' && currentIndex === 2) {
|
|
356
|
+
return filterSuggestions(['new'], args[2] || '');
|
|
357
|
+
}
|
|
358
|
+
|
|
335
359
|
// Machine subcommands
|
|
336
360
|
if (command === 'machine' && currentIndex === 1) {
|
|
337
361
|
const subcommands = ['add', 'list', 'status', 'remove', 'earmark', 'detect'];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test';
|
|
2
|
-
import { COMMANDS } from '
|
|
3
|
-
import type { CommandDef } from '
|
|
2
|
+
import { COMMANDS } from '@celilo/core';
|
|
3
|
+
import type { CommandDef } from '@celilo/core';
|
|
4
4
|
import { generateRichZshCompletion } from './generate-zsh-completion';
|
|
5
5
|
|
|
6
6
|
describe('Zsh Completion Generator', () => {
|
|
@@ -79,12 +79,15 @@ describe('Zsh Completion Generator', () => {
|
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
test('all subcommand descriptions are present', () => {
|
|
82
|
+
function escapeDesc(text: string): string {
|
|
83
|
+
return text.replace(/\\/g, '\\\\').replace(/:/g, '\\:').replace(/'/g, "'\\''");
|
|
84
|
+
}
|
|
82
85
|
function checkDescriptions(commands: CommandDef[]): void {
|
|
83
86
|
for (const cmd of commands) {
|
|
84
87
|
if (cmd.subcommands) {
|
|
85
88
|
for (const sub of cmd.subcommands) {
|
|
86
|
-
// Description should appear in the _commands function
|
|
87
|
-
expect(output).toContain(sub.description
|
|
89
|
+
// Description should appear (escaped) in the _commands function
|
|
90
|
+
expect(output).toContain(escapeDesc(sub.description));
|
|
88
91
|
}
|
|
89
92
|
checkDescriptions(cmd.subcommands);
|
|
90
93
|
}
|
|
@@ -92,4 +95,19 @@ describe('Zsh Completion Generator', () => {
|
|
|
92
95
|
}
|
|
93
96
|
checkDescriptions(COMMANDS);
|
|
94
97
|
});
|
|
98
|
+
|
|
99
|
+
test('generated completion is valid zsh syntax', () => {
|
|
100
|
+
// Recurrence gate: any unescaped quote/redirect in a description breaks the
|
|
101
|
+
// whole file (an apostrophe in "account's" once did). zsh -n parses without
|
|
102
|
+
// executing. Skip cleanly where zsh is unavailable (minimal CI images).
|
|
103
|
+
let zsh: ReturnType<typeof Bun.spawnSync>;
|
|
104
|
+
try {
|
|
105
|
+
zsh = Bun.spawnSync(['zsh', '-nc', output]);
|
|
106
|
+
} catch {
|
|
107
|
+
return; // ponytail: no zsh here — nothing to check
|
|
108
|
+
}
|
|
109
|
+
if (zsh.exitCode === null) return;
|
|
110
|
+
expect(zsh.stderr?.toString() ?? '').toBe('');
|
|
111
|
+
expect(zsh.exitCode).toBe(0);
|
|
112
|
+
});
|
|
95
113
|
});
|