@yadurajfleetos/cli 0.17.1 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/args.js CHANGED
@@ -43,7 +43,7 @@ export function parseArgs(argv) {
43
43
  export const KNOWN_FLAGS = new Set([
44
44
  // global
45
45
  'fleet', 'api', 'json', 'yes', 'y', 'help', 'h', 'version', 'v', 'no-wait',
46
- 'plan', 'dry-run', 'force',
46
+ 'plan', 'dry-run', 'force', 'dir', 'file', 'manifest',
47
47
  // per command
48
48
  'ai', 'all', 'apply', 'channel', 'deploy', 'email', 'events', 'f', 'follow', 'limit',
49
49
  'name', 'node', 'only', 'out', 'password', 'secret', 'service', 'sha',
@@ -1,5 +1,5 @@
1
1
  import { readFile, writeFile, access } from 'node:fs/promises';
2
- import { join } from 'node:path';
2
+ import { join, dirname } from 'node:path';
3
3
  import { request, requireFleet, CliError, EXIT } from '../api.js';
4
4
  import { c, table, statusColour, keyValues, relativeTime, mb } from '../render.js';
5
5
  import { task, glyph } from '../ui.js';
@@ -213,10 +213,15 @@ async function waitUntilRunning(fleetId, name, timeoutMs = 180_000) {
213
213
  * repository is legitimate for a prebuilt `image:` service, and should not
214
214
  * become an error about a file the operator never needed.
215
215
  */
216
- async function buildContextFor(serviceName) {
216
+ async function buildContextFor(serviceName, manifestPath, baseDir) {
217
217
  try {
218
- const source = await readFile('fleet.yaml', 'utf8');
219
- return planFromManifest(source).find((s) => s.name === serviceName)?.build;
218
+ const file = manifestPath ?? (baseDir ? join(baseDir, 'fleet.yaml') : 'fleet.yaml');
219
+ const source = await readFile(file, 'utf8');
220
+ const buildRel = planFromManifest(source).find((s) => s.name === serviceName)?.build;
221
+ if (!buildRel)
222
+ return undefined;
223
+ const dir = baseDir ?? (manifestPath ? dirname(manifestPath) : process.cwd());
224
+ return join(dir, buildRel);
220
225
  }
221
226
  catch {
222
227
  return undefined;
@@ -227,7 +232,7 @@ export const deployCommand = {
227
232
  const fleetId = await requireFleet(typeof flags.fleet === 'string' ? flags.fleet : undefined);
228
233
  const [name] = args;
229
234
  if (!name)
230
- throw new CliError('usage: fleet deploy <service> [--sha <git-sha>] [--no-wait]', EXIT.usage);
235
+ throw new CliError('usage: fleet deploy <service> [--sha <git-sha>] [--no-wait] [--dir <path>]', EXIT.usage);
231
236
  const service = await findService(fleetId, name);
232
237
  const gitSha = typeof flags.sha === 'string' ? flags.sha : undefined;
233
238
  const plan = await task('checking deployment plan', async () => deployPlan(fleetId, service));
@@ -252,12 +257,16 @@ export const deployCommand = {
252
257
  }
253
258
  // A service that builds from source needs its directory sent, or the
254
259
  // control plane has nothing to build and says the context does not exist.
255
- // Read from the manifest here rather than from the service row, because
256
- // the build path is relative to the file the operator is standing in.
257
260
  let contextId;
258
- const buildContext = await buildContextFor(service.name);
259
- if (buildContext) {
260
- const uploaded = await task(`packaging ${c.bold(service.name)}`, async () => uploadContext(service.id, join(process.cwd(), buildContext)), { done: (r) => `uploaded ${humanBytes(r.bytes)} of build context` });
261
+ const baseDir = typeof flags.dir === 'string' ? flags.dir : undefined;
262
+ const manifestFile = typeof flags.file === 'string'
263
+ ? flags.file
264
+ : typeof flags.manifest === 'string'
265
+ ? flags.manifest
266
+ : undefined;
267
+ const buildPath = await buildContextFor(service.name, manifestFile, baseDir);
268
+ if (buildPath) {
269
+ const uploaded = await task(`packaging ${c.bold(service.name)}`, async () => uploadContext(service.id, buildPath), { done: (r) => `uploaded ${humanBytes(r.bytes)} of build context` });
261
270
  contextId = uploaded.contextId;
262
271
  }
263
272
  const body = await withLadder(DEPLOY_STEPS, async (ladder) => {
@@ -8,7 +8,7 @@
8
8
  * the operator from the loop between them.
9
9
  */
10
10
  import { readFile, writeFile, access } from 'node:fs/promises';
11
- import { join } from 'node:path';
11
+ import { join, dirname } from 'node:path';
12
12
  import { request, requireFleet, CliError, EXIT } from '../api.js';
13
13
  import { c } from '../render.js';
14
14
  import { task, glyph } from '../ui.js';
@@ -20,7 +20,12 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
20
20
  export const upCommand = {
21
21
  async run(args, flags) {
22
22
  const fleetId = await requireFleet(typeof flags.fleet === 'string' ? flags.fleet : undefined);
23
- const manifestPath = 'fleet.yaml';
23
+ const rootDir = typeof flags.dir === 'string' ? flags.dir : process.cwd();
24
+ const manifestPath = typeof flags.file === 'string'
25
+ ? flags.file
26
+ : typeof flags.manifest === 'string'
27
+ ? flags.manifest
28
+ : join(rootDir, 'fleet.yaml');
24
29
  // ── Step 1: scaffold if needed ────────────────────────────────────
25
30
  let needsApply = false;
26
31
  try {
@@ -29,14 +34,14 @@ export const upCommand = {
29
34
  catch {
30
35
  // No fleet.yaml — run the smart init inline.
31
36
  const { detect, manifestTemplate } = await import('../detect.js');
32
- const d = await task('detecting project framework', async () => detect());
37
+ const d = await task('detecting project framework', async () => detect(rootDir));
33
38
  const name = (typeof flags.name === 'string' ? flags.name : '') ||
34
39
  args[0] ||
35
- process.cwd().split('/').pop()?.toLowerCase().replace(/[^a-z0-9-]+/g, '-') ||
40
+ rootDir.split('/').pop()?.toLowerCase().replace(/[^a-z0-9-]+/g, '-') ||
36
41
  'app';
37
42
  // Write Dockerfile if generated
38
43
  if (d.dockerfile) {
39
- await writeFile(join(process.cwd(), 'Dockerfile'), d.dockerfile);
44
+ await writeFile(join(rootDir, 'Dockerfile'), d.dockerfile);
40
45
  console.log(`${glyph.ok} ${c.green('created')} Dockerfile ${c.dim(`(${d.label}, port ${d.port})`)}`);
41
46
  }
42
47
  // Write manifest
@@ -47,7 +52,7 @@ export const upCommand = {
47
52
  // ── Step 2: read and apply the manifest ───────────────────────────
48
53
  const manifest = await readFile(manifestPath, 'utf8');
49
54
  const applyResult = await task(`applying ${manifestPath}`, async () => (await request('POST', `/fleets/${fleetId}/services`, {
50
- body: { manifest, project: projectNameFor(process.cwd()) },
55
+ body: { manifest, project: projectNameFor(rootDir) },
51
56
  })).body, {
52
57
  done: (b) => b.created.length || b.updated.length
53
58
  ? `applied ${b.created.length + b.updated.length} service(s) to project ${b.project}`
@@ -76,13 +81,6 @@ export const upCommand = {
76
81
  return service;
77
82
  });
78
83
  // A database that is already serving is left alone.
79
- //
80
- // Redeploying one replaces a running container for no reason, and every
81
- // service that talks to it loses its connections while it restarts. It is
82
- // in the plan so that a database which is *not* running comes back — which
83
- // is the case that used to need `fleet up db` by name — not so that every
84
- // deploy of the stack restarts the database underneath it. Naming it
85
- // explicitly still redeploys it.
86
84
  const skipped = args[0]
87
85
  ? []
88
86
  : resolved.filter((s) => isDatabase.has(s.name) && s.current?.status === 'running');
@@ -101,6 +99,7 @@ export const upCommand = {
101
99
  gitSha,
102
100
  buildContext: buildContexts.get(service.name),
103
101
  wait: !flags['no-wait'],
102
+ rootDir: typeof flags.file === 'string' ? dirname(flags.file) : rootDir,
104
103
  });
105
104
  deployed.push({ service, url });
106
105
  }
@@ -123,18 +122,11 @@ export const upCommand = {
123
122
  /**
124
123
  * Deploy one service: upload its build context if it has one, run the deploy,
125
124
  * and wait for it to report running.
126
- *
127
- * Returns the URL the control plane handed back, or null for a service that
128
- * has none — an internal one, which is reached by name from its neighbours
129
- * rather than from outside.
130
125
  */
131
126
  async function deployOne(service, opts) {
132
- // A service that builds from source sends its directory first. The control
133
- // plane then builds it for every architecture the fleet has, which is the
134
- // part that is easy to get wrong by hand and silent when you do.
135
127
  let contextId;
136
128
  if (opts.buildContext) {
137
- const dir = join(process.cwd(), opts.buildContext);
129
+ const dir = join(opts.rootDir, opts.buildContext);
138
130
  const uploaded = await task(`packaging ${c.bold(service.name)}`, async () => uploadContext(service.id, dir), { done: (r) => `uploaded ${humanBytes(r.bytes)} of build context` });
139
131
  contextId = uploaded.contextId;
140
132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yadurajfleetos/cli",
3
- "version": "0.17.1",
3
+ "version": "0.17.2",
4
4
  "description": "Fleet OS command-line interface for deploying and orchestrating services on user-owned hardware",
5
5
  "type": "module",
6
6
  "license": "MIT",