@junoflow/cli 0.3.0 → 0.6.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.
@@ -0,0 +1,60 @@
1
+ import { randomUUID } from 'node:crypto';
2
+
3
+ import { connect } from '../api.js';
4
+ import { UsageError } from '../errors.js';
5
+ import { converse, DEFAULT_WAIT_SECONDS, finish } from '../prompt.js';
6
+
7
+ export const ASK_USAGE =
8
+ 'juno ask ["<prompt>"] [--session <id>] [--wait <seconds>]';
9
+
10
+ /**
11
+ * `juno ask` is the CLI for the Agent Prompt API: it prompts the in-app
12
+ * assistant and prints the turn's final event. Off a TTY each call is one
13
+ * `stream: false` request that waits up to --wait seconds; on one it streams
14
+ * and writes progress to stderr. The exit code is the outcome (prompt.js).
15
+ *
16
+ * `--session <id>` resumes a conversation; with no prompt it observes, which
17
+ * is how to keep waiting on a turn that answered `pending`. A new
18
+ * conversation's id is minted here, so a transport retry resumes it rather
19
+ * than starting a second.
20
+ */
21
+ export async function ask(args, io) {
22
+ const prompt = args._.length ? args._.join(' ') : undefined;
23
+ if (prompt === undefined && args.session === undefined) {
24
+ throw new UsageError(
25
+ `juno ask needs a prompt, or --session <id> to observe a session\nusage: ${ASK_USAGE}`
26
+ );
27
+ }
28
+ if (prompt !== undefined && !prompt.trim()) {
29
+ throw new UsageError('the prompt is empty');
30
+ }
31
+ // To a session awaiting approval, "approve" lets a tool call through. That
32
+ // is `juno session approve`, so a rule allowing `juno ask` cannot do it.
33
+ if (prompt?.trim().toLowerCase() === 'approve') {
34
+ throw new UsageError(
35
+ 'juno ask does not approve tool calls; use: juno session approve ' +
36
+ (args.session ?? '<session-id>')
37
+ );
38
+ }
39
+
40
+ const stream = io.isTTY;
41
+ const body = { session_id: args.session ?? randomUUID() };
42
+ if (prompt !== undefined) {
43
+ body.message = prompt;
44
+ body.message_id = randomUUID();
45
+ }
46
+ if (!stream) body.wait_timeout_seconds = waitSeconds(args.wait);
47
+
48
+ const { client } = await connect(args, io);
49
+ const outcome = await converse(client, body, { stream, io });
50
+ return finish(outcome, body.session_id, io);
51
+ }
52
+
53
+ /** waitSeconds parses --wait; the server bounds it (1–600) and says so. */
54
+ export function waitSeconds(flag) {
55
+ if (flag === undefined) return DEFAULT_WAIT_SECONDS;
56
+ if (!/^\d+$/.test(flag) || Number(flag) < 1) {
57
+ throw new UsageError(`--wait takes a number of seconds, got "${flag}"`);
58
+ }
59
+ return Number(flag);
60
+ }
@@ -1,19 +1,26 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { join } from 'node:path';
3
3
 
4
- import { JunoClient } from '../api.js';
5
- import { UserError } from '../errors.js';
4
+ import { clientName, JunoClient } from '../api.js';
5
+ import { printDiagnosticsJSON, printJSON } from '../diagnostics.js';
6
+ import { DiagnosticsError, UserError } from '../errors.js';
6
7
  import { readProject } from '../project.js';
7
8
  import { resolveTarget } from '../target.js';
8
9
  import { verifyTypes } from './verify-types.js';
9
10
 
11
+ export const CHECK_USAGE =
12
+ 'juno integration check [--skip-local] [--id <id>] [--json]';
13
+
10
14
  /**
11
- * `juno check` — the offline checks, then the ones only a Juno can answer.
15
+ * `juno integration check` — the offline checks, then the ones only a Juno can answer.
12
16
  *
13
17
  * tsc and eslint run locally against the committed types; reserved names,
14
18
  * cross-surface collisions and JSON Schema compilation need the server, so
15
19
  * they go through ingest?dry_run=1, which stores nothing and is safe to point
16
20
  * at a production instance.
21
+ *
22
+ * --json prints the route's body (or the 422's diagnostics) on stdout and
23
+ * nothing else; the local tools' own output goes to stderr.
17
24
  */
18
25
  export async function check(argv, io) {
19
26
  let failed = false;
@@ -22,7 +29,8 @@ export async function check(argv, io) {
22
29
  // Before tsc, deliberately: type-checking against types/ says nothing if
23
30
  // types/ describes a manifest that is no longer on disk.
24
31
  try {
25
- await verifyTypes(argv, io);
32
+ // verify-types reports success on stdout, which --json reserves.
33
+ await verifyTypes(argv, argv.json ? { ...io, log: io.warn } : io);
26
34
  } catch (err) {
27
35
  if (!(err instanceof UserError)) throw err;
28
36
  io.error(err.message);
@@ -32,7 +40,9 @@ export async function check(argv, io) {
32
40
  ['typecheck', 'tsc', ['--noEmit']],
33
41
  ['lint', 'eslint', ['.']],
34
42
  ]) {
35
- const code = await runLocal(bin, args, io);
43
+ const code = await runLocal(bin, args, io, {
44
+ quietStdout: argv.json,
45
+ });
36
46
  if (code === SKIPPED) {
37
47
  io.warn(`${label}: ${bin} is not installed — run npm install`);
38
48
  continue;
@@ -41,30 +51,48 @@ export async function check(argv, io) {
41
51
  }
42
52
  }
43
53
 
44
- const target = await resolveTarget({ url: argv.url, cwd: io.cwd });
45
- const client = argv.client ?? new JunoClient(target);
46
- const project = await readProject(io.cwd);
47
- const result = await client.ingest(project, {
48
- dryRun: true,
49
- name: argv.name,
54
+ const target = await resolveTarget({
55
+ url: argv.url,
56
+ cwd: io.cwd,
57
+ env: io.env,
50
58
  });
51
- for (const warning of result.warnings ?? []) io.warn(`warning: ${warning}`);
52
- io.log(`${result.name}: manifest and sources verified against ${target}`);
59
+ const client =
60
+ argv.client ?? new JunoClient(target, { client: clientName(io.env) });
61
+ const project = await readProject(io.cwd);
62
+ let result;
63
+ try {
64
+ result = await client.ingest(project, { dryRun: true, id: argv.id });
65
+ } catch (err) {
66
+ if (argv.json && err instanceof DiagnosticsError) {
67
+ return printDiagnosticsJSON(err, io);
68
+ }
69
+ throw err;
70
+ }
71
+
72
+ if (argv.json) {
73
+ printJSON(result, io);
74
+ } else {
75
+ for (const w of result.warnings ?? []) io.warn(`warning: ${w}`);
76
+ io.log(`${result.id}: manifest and sources verified against ${target}`);
77
+ }
53
78
 
54
79
  return failed ? 1 : 0;
55
80
  }
56
81
 
57
- const SKIPPED = -1;
82
+ export const SKIPPED = -1;
58
83
 
59
84
  /**
60
85
  * Runs a local devtool from node_modules/.bin. Its output goes straight to the
61
86
  * terminal: tsc and eslint already print file:line:col, and reformatting them
62
87
  * would only lose the column.
63
88
  */
64
- function runLocal(bin, args, io) {
89
+ export function runLocal(bin, args, io, { quietStdout = false } = {}) {
65
90
  const path = join(io.cwd, 'node_modules', '.bin', bin);
91
+ // quietStdout sends the tool's stdout to stderr, for a --json run whose
92
+ // stdout must carry the JSON and nothing else.
93
+ const stdio = quietStdout ? ['inherit', 2, 'inherit'] : 'inherit';
66
94
  return new Promise((resolve) => {
67
- const child = spawn(path, args, { cwd: io.cwd, stdio: 'inherit' });
95
+ const child = spawn(path, args, { cwd: io.cwd, stdio });
68
96
  child.on('error', () => resolve(SKIPPED));
69
97
  child.on('close', (code) => resolve(code ?? 1));
70
98
  });
@@ -0,0 +1,26 @@
1
+ import { readUserConfig, writeUserConfig } from '../config.js';
2
+ import { UsageError } from '../errors.js';
3
+ import { normalizeTarget } from '../target.js';
4
+
5
+ export const CONFIG_SET_USAGE = 'juno config set url <url>';
6
+
7
+ /**
8
+ * `juno config set url <url>` writes the user config file, the last place
9
+ * resolveTarget looks. `url` is the only key; it is normalised the way every
10
+ * other source is, so what lands in the file is what a request would use.
11
+ */
12
+ export async function configSet(argv, io) {
13
+ const [key, value, ...extra] = argv._;
14
+ const usage = `usage: ${CONFIG_SET_USAGE}`;
15
+ if (key === undefined) throw new UsageError(usage);
16
+ if (key !== 'url') {
17
+ throw new UsageError(`unknown config key "${key}"; ${usage}`);
18
+ }
19
+ if (value === undefined || extra.length) throw new UsageError(usage);
20
+
21
+ const url = normalizeTarget(value);
22
+ const current = await readUserConfig(io.env);
23
+ const path = await writeUserConfig(io.env, { ...current, url });
24
+ io.warn(`wrote ${path}`);
25
+ return 0;
26
+ }
@@ -0,0 +1,68 @@
1
+ import { connect } from '../api.js';
2
+ import { UsageError } from '../errors.js';
3
+
4
+ export const CREDENTIAL_SET_USAGE =
5
+ 'juno credential set <id> [--name <name>] [--description <text>]';
6
+
7
+ /**
8
+ * `juno credential set <id>` stores a secret a person types at a terminal.
9
+ * It goes through the REST credential routes rather than the tool gateway,
10
+ * which deliberately serves no write_credential: a secret an agent types
11
+ * lands in its transcript. For the same reason it refuses to run unless stdin
12
+ * is a TTY, so nothing can pipe a value in, and the value is read with echo
13
+ * off and never printed.
14
+ *
15
+ * An existing credential keeps its name and description unless --name or
16
+ * --description overrides them; a new one needs both.
17
+ */
18
+ export async function credentialSet(args, io) {
19
+ const [id, ...extra] = args._;
20
+ if (id === undefined || extra.length) {
21
+ throw new UsageError(`usage: ${CREDENTIAL_SET_USAGE}`);
22
+ }
23
+ if (!io.stdinIsTTY) {
24
+ throw new UsageError(
25
+ 'juno credential set reads the secret from a terminal, and stdin is not ' +
26
+ 'one. A person must run it, e.g. as ' +
27
+ `\`! juno credential set ${id}\` inside Claude Code.`
28
+ );
29
+ }
30
+
31
+ const { client } = await connect(args, io);
32
+ const existing = await client.getCredentialMeta(id);
33
+
34
+ // Everything that can refuse the command does so before the prompt, so
35
+ // nobody types a secret into a command that was never going to store it.
36
+ if (!existing) {
37
+ const missing = ['name', 'description']
38
+ .filter((flag) => args[flag] === undefined)
39
+ .map((flag) => `--${flag}`);
40
+ if (missing.length) {
41
+ throw new UsageError(
42
+ `credential "${id}" does not exist; creating it needs --name and ` +
43
+ `--description (missing: ${missing.join(', ')})`
44
+ );
45
+ }
46
+ }
47
+
48
+ const value = await io.readSecret(`Secret for credential "${id}": `);
49
+ if (!value) throw new UsageError('an empty secret was not stored');
50
+
51
+ if (existing) {
52
+ await client.updateCredential(id, {
53
+ name: args.name ?? existing.name,
54
+ description: args.description ?? existing.description,
55
+ value,
56
+ });
57
+ io.warn(`updated credential "${id}"`);
58
+ } else {
59
+ await client.createCredential({
60
+ id,
61
+ name: args.name,
62
+ description: args.description,
63
+ value,
64
+ });
65
+ io.warn(`created credential "${id}"`);
66
+ }
67
+ return 0;
68
+ }
@@ -1,30 +1,45 @@
1
- import { watch } from 'node:fs';
2
1
  import { join } from 'node:path';
3
2
 
4
- import { JunoClient } from '../api.js';
3
+ import { clientName, JunoClient } from '../api.js';
4
+ import { printDiagnosticsJSON, printJSON } from '../diagnostics.js';
5
5
  import { DiagnosticsError } from '../errors.js';
6
6
  import { readManifestRaw, readProject, writeTypes } from '../project.js';
7
7
  import { resolveTarget } from '../target.js';
8
8
  import { stampTypeFiles } from '../stamp.js';
9
- import { formatDiagnostics } from '../diagnostics.js';
10
- import { pickTypeFiles } from './types.js';
9
+ import { pickTypeFiles } from './gen-types.js';
10
+ import { deployOnChange } from '../watch.js';
11
11
 
12
- /** How long to wait for edits to settle before redeploying. */
13
- const WATCH_DEBOUNCE_MS = 200;
12
+ export const DEPLOY_USAGE =
13
+ 'juno integration deploy [--watch] [--id <id>] [--json]';
14
14
 
15
15
  /**
16
- * `juno deploy [--watch]` — upload the working copy, verify, activate.
16
+ * `juno integration deploy [--watch]` — upload the working copy, verify, activate.
17
17
  *
18
18
  * The response carries the regenerated types, so a deploy keeps the committed
19
19
  * types/ honest without a second round trip that could answer for a different
20
20
  * manifest.
21
+ *
22
+ * --json prints the route's body (or the 422's diagnostics) on stdout instead
23
+ * of the summary line, for CI.
21
24
  */
22
25
  export async function deploy(argv, io) {
23
- const target = await resolveTarget({ url: argv.url, cwd: io.cwd });
24
- const client = argv.client ?? new JunoClient(target);
26
+ const target = await resolveTarget({
27
+ url: argv.url,
28
+ cwd: io.cwd,
29
+ env: io.env,
30
+ });
31
+ const client =
32
+ argv.client ?? new JunoClient(target, { client: clientName(io.env) });
25
33
 
26
34
  if (!argv.watch) {
27
- await deployOnce(client, argv, io, target);
35
+ try {
36
+ await deployOnce(client, argv, io, target);
37
+ } catch (err) {
38
+ if (argv.json && err instanceof DiagnosticsError) {
39
+ return printDiagnosticsJSON(err, io);
40
+ }
41
+ throw err;
42
+ }
28
43
  return 0;
29
44
  }
30
45
  return watchAndDeploy(client, argv, io, target);
@@ -32,16 +47,20 @@ export async function deploy(argv, io) {
32
47
 
33
48
  async function deployOnce(client, argv, io, target) {
34
49
  const project = await readProject(io.cwd);
35
- const result = await client.ingest(project, { name: argv.name });
50
+ const result = await client.ingest(project, { id: argv.id });
36
51
  // Stamped against the manifest this deploy was built from, so the watch loop
37
- // leaves a tree that `juno verify-types` accepts.
52
+ // leaves a tree that `juno integration verify-types` accepts.
38
53
  const written = await writeTypes(
39
54
  stampTypeFiles(pickTypeFiles(result.types), await readManifestRaw(io.cwd)),
40
55
  io.cwd
41
56
  );
57
+ if (argv.json) {
58
+ printJSON(result, io);
59
+ return result;
60
+ }
42
61
  for (const warning of result.warnings ?? []) io.warn(`warning: ${warning}`);
43
62
  io.log(
44
- `deployed ${result.name} v${result.version} to ${target} (${written.join(', ')})`
63
+ `deployed ${result.id} v${result.version} to ${target} (${written.join(', ')})`
45
64
  );
46
65
  return result;
47
66
  }
@@ -49,43 +68,15 @@ async function deployOnce(client, argv, io, target) {
49
68
  /**
50
69
  * Watch mode is the loop the whole design is for: edit the manifest, the types
51
70
  * regenerate, and the editor red-squiggles the method you have not written yet.
52
- * A failed deploy prints and keeps watching — the next save is the retry.
53
71
  */
54
- async function watchAndDeploy(client, argv, io, target) {
55
- io.log(`watching ${io.cwd} — deploying to ${target}`);
56
- await runWatchIteration(client, argv, io, target);
57
-
58
- let timer = null;
59
- const schedule = () => {
60
- if (timer) clearTimeout(timer);
61
- timer = setTimeout(() => {
62
- timer = null;
63
- void runWatchIteration(client, argv, io, target);
64
- }, WATCH_DEBOUNCE_MS);
65
- };
66
-
67
- const watchers = [
68
- watch(join(io.cwd, 'src'), { recursive: true }, schedule),
69
- watch(join(io.cwd, 'manifest.json'), schedule),
70
- ];
71
- // Resolves only on SIGINT: watch mode ends when the author ends it.
72
- await new Promise((resolve) => {
73
- process.once('SIGINT', () => {
74
- for (const w of watchers) w.close();
75
- resolve();
76
- });
72
+ function watchAndDeploy(client, argv, io, target) {
73
+ return deployOnChange({
74
+ io,
75
+ target,
76
+ paths: [
77
+ { path: join(io.cwd, 'src'), recursive: true },
78
+ { path: join(io.cwd, 'manifest.json') },
79
+ ],
80
+ once: () => deployOnce(client, argv, io, target),
77
81
  });
78
- return 0;
79
- }
80
-
81
- async function runWatchIteration(client, argv, io, target) {
82
- try {
83
- await deployOnce(client, argv, io, target);
84
- } catch (err) {
85
- if (err instanceof DiagnosticsError) {
86
- io.error(formatDiagnostics(err.diagnostics));
87
- } else {
88
- io.error(err.message);
89
- }
90
- }
91
82
  }
@@ -0,0 +1,93 @@
1
+ import { connect } from '../api.js';
2
+ import { DiagnosticsError } from '../errors.js';
3
+ import { readFlowProject } from '../flow-project.js';
4
+ import { runLocal, SKIPPED } from './check.js';
5
+ import { printDiagnosticsJSON, printJSON } from '../diagnostics.js';
6
+ import { printWarnings } from './flow-deploy.js';
7
+
8
+ export const FLOW_CHECK_USAGE = 'juno flow check [--skip-local] [--json]';
9
+
10
+ /**
11
+ * `juno flow check` — local tsc and eslint, then a dry-run ingest: the same
12
+ * verification deploy runs (and the base_version check, so a stale copy is
13
+ * reported here before deploy hits it), with nothing committed. Safe to point
14
+ * at a production instance.
15
+ */
16
+ export async function flowCheck(argv, io) {
17
+ const project = await readFlowProject(io.cwd);
18
+ let failed = false;
19
+
20
+ if (!argv['skip-local']) {
21
+ for (const [label, bin, args] of [
22
+ ['typecheck', 'tsc', ['--noEmit']],
23
+ ['lint', 'eslint', ['.']],
24
+ ]) {
25
+ const code = await runLocal(bin, args, io, {
26
+ quietStdout: argv.json,
27
+ });
28
+ if (code === SKIPPED) {
29
+ io.warn(`${label}: ${bin} is not installed — run npm install`);
30
+ continue;
31
+ }
32
+ if (code !== 0) failed = true;
33
+ }
34
+ }
35
+ warnOnDependencyDrift(project, io);
36
+
37
+ const { target, client } = await connect(argv, io);
38
+ let result;
39
+ try {
40
+ result = await client.ingestFlow(
41
+ {
42
+ flowId: project.flowId,
43
+ manifest: project.manifest,
44
+ flowTS: project.flowTS,
45
+ baseVersion: project.baseVersion,
46
+ },
47
+ { dryRun: true }
48
+ );
49
+ } catch (err) {
50
+ if (argv.json && err instanceof DiagnosticsError) {
51
+ return printDiagnosticsJSON(err, io);
52
+ }
53
+ throw err;
54
+ }
55
+
56
+ if (argv.json) {
57
+ printJSON(result, io);
58
+ } else {
59
+ printWarnings(result.warnings, io);
60
+ io.log(
61
+ `${result.flow_id}: manifest and flow.ts verified against ${target}`
62
+ );
63
+ }
64
+ return failed ? 1 : 0;
65
+ }
66
+
67
+ /**
68
+ * manifest.dependencies is what verify installs; package.json's dependencies
69
+ * are only there so a local `npm install` gives tsc the same modules. When the
70
+ * two disagree, local and server-side checks are checking different programs,
71
+ * so say which packages differ — and that the manifest is the one to edit.
72
+ */
73
+ export function warnOnDependencyDrift(project, io) {
74
+ const declared = project.manifest?.dependencies ?? {};
75
+ const installed = project.packageJSON?.dependencies ?? {};
76
+ const names = [
77
+ ...new Set([...Object.keys(declared), ...Object.keys(installed)]),
78
+ ].sort();
79
+ const diffs = [];
80
+ for (const name of names) {
81
+ const a = declared[name];
82
+ const b = installed[name];
83
+ if (a === b) continue;
84
+ if (b === undefined) diffs.push(`${name} (only in manifest.json)`);
85
+ else if (a === undefined) diffs.push(`${name} (only in package.json)`);
86
+ else diffs.push(`${name} (manifest.json ${a}, package.json ${b})`);
87
+ }
88
+ if (diffs.length === 0) return;
89
+ io.warn(
90
+ `warning: manifest.json and package.json list different dependencies: ${diffs.join(', ')}. ` +
91
+ "Juno installs manifest.json's; add packages there, then copy them to package.json (or run `npm run pull` after deploying)."
92
+ );
93
+ }
@@ -0,0 +1,107 @@
1
+ import { join } from 'node:path';
2
+
3
+ import { connect } from '../api.js';
4
+ import { DiagnosticsError } from '../errors.js';
5
+ import {
6
+ formatDiagnostic,
7
+ printDiagnosticsJSON,
8
+ printJSON,
9
+ } from '../diagnostics.js';
10
+ import {
11
+ FLOW_TS,
12
+ MANIFEST,
13
+ manifestText,
14
+ readFlowProject,
15
+ typeFiles,
16
+ updatePackageJSON,
17
+ writeFlowFiles,
18
+ } from '../flow-project.js';
19
+ import { deployOnChange } from '../watch.js';
20
+
21
+ export const FLOW_DEPLOY_USAGE =
22
+ 'juno flow deploy [--watch] [--force] [--json]';
23
+
24
+ /**
25
+ * `juno flow deploy` — upload manifest.json and flow.ts to
26
+ * POST /api/flows/ingest, which verifies and commits them through the same
27
+ * core the in-app assistant's submit_code uses, and activates the version.
28
+ *
29
+ * It sends `juno.base_version`, so a flow edited elsewhere since this copy was
30
+ * pulled is a 409 (exit 4) rather than a silent overwrite; --force sends none
31
+ * and replaces whatever is active. On success it writes back what the server
32
+ * now holds: the canonical manifest, both .d.ts files, and the new
33
+ * base_version.
34
+ */
35
+ export async function flowDeploy(argv, io) {
36
+ const { target, client } = await connect(argv, io);
37
+ if (!argv.watch) {
38
+ return deployOnce(client, argv, io, target);
39
+ }
40
+
41
+ // A deploy writes manifest.json back, which the watcher sees. Remembering
42
+ // what was last deployed makes that echo — and any save that changed
43
+ // nothing — a no-op instead of another version.
44
+ let last = null;
45
+ return deployOnChange({
46
+ io,
47
+ target,
48
+ paths: [{ path: join(io.cwd, FLOW_TS) }, { path: join(io.cwd, MANIFEST) }],
49
+ once: async () => {
50
+ const project = await readFlowProject(io.cwd);
51
+ const key = JSON.stringify([project.manifestRaw, project.flowTS]);
52
+ if (key === last) return;
53
+ const after = await deployProject(client, project, argv, io, target);
54
+ last = JSON.stringify([after.manifestRaw, project.flowTS]);
55
+ },
56
+ });
57
+ }
58
+
59
+ async function deployOnce(client, argv, io, target) {
60
+ const project = await readFlowProject(io.cwd);
61
+ try {
62
+ await deployProject(client, project, argv, io, target);
63
+ } catch (err) {
64
+ if (argv.json && err instanceof DiagnosticsError) {
65
+ return printDiagnosticsJSON(err, io);
66
+ }
67
+ throw err;
68
+ }
69
+ return 0;
70
+ }
71
+
72
+ async function deployProject(client, project, argv, io, target) {
73
+ const result = await client.ingestFlow({
74
+ flowId: project.flowId,
75
+ manifest: project.manifest,
76
+ flowTS: project.flowTS,
77
+ baseVersion: argv.force ? undefined : project.baseVersion,
78
+ });
79
+
80
+ const manifestRaw = manifestText(result.manifest);
81
+ const written = await writeFlowFiles(io.cwd, {
82
+ [MANIFEST]: manifestRaw,
83
+ ...typeFiles(result.types),
84
+ });
85
+ if (project.baseVersion !== result.version) {
86
+ await updatePackageJSON(io.cwd, (pkg) => {
87
+ pkg.juno.base_version = result.version;
88
+ });
89
+ written.push('package.json');
90
+ }
91
+
92
+ if (argv.json) {
93
+ printJSON(result, io);
94
+ } else {
95
+ printWarnings(result.warnings, io);
96
+ const files = written.length ? ` (${written.sort().join(', ')})` : '';
97
+ io.log(
98
+ `deployed ${result.flow_id} v${result.version} to ${target}${files}`
99
+ );
100
+ }
101
+ return { manifestRaw };
102
+ }
103
+
104
+ /** A route's warnings, each as a diagnostic line with a `warning: ` prefix. */
105
+ export function printWarnings(warnings, io) {
106
+ for (const w of warnings ?? []) io.warn(`warning: ${formatDiagnostic(w)}`);
107
+ }
@@ -0,0 +1,30 @@
1
+ import { connect } from '../api.js';
2
+ import {
3
+ readFlowManifest,
4
+ readFlowPackage,
5
+ typeFiles,
6
+ writeFlowFiles,
7
+ } from '../flow-project.js';
8
+
9
+ export const FLOW_GEN_TYPES_USAGE = 'juno flow gen-types';
10
+
11
+ /**
12
+ * `juno flow gen-types` — regenerate flow-env.d.ts and flow-api.d.ts from
13
+ * manifest.json against the instance's live catalog. manifest.json itself is
14
+ * left alone: this refreshes what the editor type-checks against, nothing
15
+ * more. Stale types cannot ship regardless, because ingest regenerates them.
16
+ */
17
+ export async function flowGenTypes(argv, io) {
18
+ await readFlowPackage(io.cwd);
19
+ const { manifest } = await readFlowManifest(io.cwd);
20
+ const { client } = await connect(argv, io);
21
+ const generated = await client.typegenFlow(manifest);
22
+ const files = typeFiles(generated?.types);
23
+ const written = await writeFlowFiles(io.cwd, files);
24
+ io.log(
25
+ written.length
26
+ ? `wrote ${written.join(', ')}`
27
+ : `${Object.keys(files).sort().join(', ')} already up to date`
28
+ );
29
+ return 0;
30
+ }