@junoflow/cli 0.1.5 → 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.
- package/README.md +579 -54
- package/bin/juno.js +7 -0
- package/eslint.flow.config.js +33 -0
- package/package.json +7 -4
- package/src/api.js +395 -27
- package/src/authoring-skills.js +42 -0
- package/src/commands/agent-skill-install.js +64 -0
- package/src/commands/ask.js +60 -0
- package/src/commands/check.js +44 -16
- package/src/commands/config.js +26 -0
- package/src/commands/credential-set.js +68 -0
- package/src/commands/deploy.js +41 -50
- package/src/commands/flow-check.js +93 -0
- package/src/commands/flow-deploy.js +107 -0
- package/src/commands/flow-gen-types.js +30 -0
- package/src/commands/flow-init.js +59 -0
- package/src/commands/flow-pull.js +86 -0
- package/src/commands/{types.js → gen-types.js} +12 -5
- package/src/commands/init.js +28 -16
- package/src/commands/pull.js +70 -16
- package/src/commands/session-approve.js +73 -0
- package/src/commands/skill.js +73 -0
- package/src/commands/tool.js +249 -0
- package/src/commands/verify-types.js +4 -2
- package/src/config.js +34 -0
- package/src/diagnostics.js +14 -0
- package/src/errors.js +40 -0
- package/src/exit.js +22 -0
- package/src/flow-project.js +128 -0
- package/src/help.js +199 -0
- package/src/index.js +506 -75
- package/src/installed-skill.js +55 -0
- package/src/prompt.js +196 -0
- package/src/stamp.js +10 -9
- package/src/suggest.js +67 -0
- package/src/target.js +22 -7
- package/src/verbs.js +34 -0
- package/src/watch.js +50 -0
- package/verbs.json +227 -0
|
@@ -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
|
+
}
|
package/src/commands/check.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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({
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
|
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
|
+
}
|
package/src/commands/deploy.js
CHANGED
|
@@ -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 {
|
|
10
|
-
import {
|
|
9
|
+
import { pickTypeFiles } from './gen-types.js';
|
|
10
|
+
import { deployOnChange } from '../watch.js';
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
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({
|
|
24
|
-
|
|
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
|
-
|
|
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, {
|
|
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.
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
}
|