@junoflow/cli 0.3.0 → 0.9.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 +693 -52
- package/app-client.js +306 -0
- package/bin/juno.js +7 -0
- package/eslint.app.config.js +34 -0
- package/eslint.flow.config.js +35 -0
- package/package.json +12 -4
- package/src/api.js +541 -30
- package/src/app-dev/build.js +134 -0
- package/src/app-dev/harness.js +108 -0
- package/src/app-dev/panel.html +336 -0
- package/src/app-dev/panel.js +290 -0
- package/src/app-dev/server.js +347 -0
- package/src/app-dev/stream.js +224 -0
- package/src/app-dev/watch.js +47 -0
- package/src/app-project.js +133 -0
- package/src/authoring-skills.js +43 -0
- package/src/commands/agent-skill-install.js +64 -0
- package/src/commands/app-check.js +75 -0
- package/src/commands/app-deploy.js +102 -0
- package/src/commands/app-dev.js +141 -0
- package/src/commands/app-gen-types.js +37 -0
- package/src/commands/app-init.js +46 -0
- package/src/commands/app-pull.js +106 -0
- package/src/commands/ask.js +60 -0
- package/src/commands/check.js +71 -27
- package/src/commands/config.js +26 -0
- package/src/commands/credential-set.js +68 -0
- package/src/commands/deploy.js +95 -51
- package/src/commands/flow-check.js +75 -0
- package/src/commands/flow-deploy.js +110 -0
- package/src/commands/flow-fixture.js +67 -0
- package/src/commands/flow-gen-types.js +37 -0
- package/src/commands/flow-init.js +59 -0
- package/src/commands/flow-pull.js +115 -0
- package/src/commands/{types.js → gen-types.js} +12 -5
- package/src/commands/init.js +28 -16
- package/src/commands/pull.js +111 -63
- 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 +78 -0
- package/src/help.js +199 -0
- package/src/index.js +567 -75
- package/src/installed-skill.js +55 -0
- package/src/project.js +76 -8
- 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 +87 -0
- package/src/working-copy.js +262 -0
- package/tsconfig.page.json +15 -0
- package/verbs.json +276 -0
package/src/commands/deploy.js
CHANGED
|
@@ -1,91 +1,135 @@
|
|
|
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
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
readManifestRaw,
|
|
8
|
+
readProject,
|
|
9
|
+
updateIntegrationPackage,
|
|
10
|
+
writeTypes,
|
|
11
|
+
} from '../project.js';
|
|
7
12
|
import { resolveTarget } from '../target.js';
|
|
8
13
|
import { stampTypeFiles } from '../stamp.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
14
|
+
import { pickTypeFiles } from './gen-types.js';
|
|
15
|
+
import { deployOnChange } from '../watch.js';
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
export const DEPLOY_USAGE =
|
|
18
|
+
'juno integration deploy [--watch] [--force] [--id <id>] [--json]';
|
|
14
19
|
|
|
15
20
|
/**
|
|
16
|
-
* `juno deploy [--watch]` — upload the working copy, verify, activate.
|
|
21
|
+
* `juno integration deploy [--watch]` — upload the working copy, verify, activate.
|
|
22
|
+
*
|
|
23
|
+
* It sends `juno.base_version`, so an install changed elsewhere since this
|
|
24
|
+
* copy was pulled is a 409 (exit 4) rather than a silent overwrite; --force
|
|
25
|
+
* sends none and replaces whatever is active. A successful deploy writes the
|
|
26
|
+
* new version back as the base. --id naming another install sends no base and
|
|
27
|
+
* writes none back: the base belongs to the install the manifest names (see
|
|
28
|
+
* baseFor).
|
|
17
29
|
*
|
|
18
30
|
* The response carries the regenerated types, so a deploy keeps the committed
|
|
19
31
|
* types/ honest without a second round trip that could answer for a different
|
|
20
32
|
* manifest.
|
|
33
|
+
*
|
|
34
|
+
* --json prints the route's body (or the 422's diagnostics) on stdout instead
|
|
35
|
+
* of the summary line, for CI.
|
|
21
36
|
*/
|
|
22
37
|
export async function deploy(argv, io) {
|
|
23
|
-
const target = await resolveTarget({
|
|
24
|
-
|
|
38
|
+
const target = await resolveTarget({
|
|
39
|
+
url: argv.url,
|
|
40
|
+
cwd: io.cwd,
|
|
41
|
+
env: io.env,
|
|
42
|
+
});
|
|
43
|
+
const client =
|
|
44
|
+
argv.client ?? new JunoClient(target, { client: clientName(io.env) });
|
|
25
45
|
|
|
26
46
|
if (!argv.watch) {
|
|
27
|
-
|
|
47
|
+
try {
|
|
48
|
+
await deployOnce(client, argv, io, target);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (argv.json && err instanceof DiagnosticsError) {
|
|
51
|
+
return printDiagnosticsJSON(err, io);
|
|
52
|
+
}
|
|
53
|
+
throw err;
|
|
54
|
+
}
|
|
28
55
|
return 0;
|
|
29
56
|
}
|
|
30
57
|
return watchAndDeploy(client, argv, io, target);
|
|
31
58
|
}
|
|
32
59
|
|
|
33
60
|
async function deployOnce(client, argv, io, target) {
|
|
61
|
+
// Re-read on every deploy, base included, so the watch loop is checked
|
|
62
|
+
// against its own previous deploy rather than refused by it.
|
|
34
63
|
const project = await readProject(io.cwd);
|
|
35
|
-
const result = await client.ingest(project, {
|
|
64
|
+
const result = await client.ingest(project, {
|
|
65
|
+
id: argv.id,
|
|
66
|
+
baseVersion: argv.force ? undefined : baseFor(project, argv, io),
|
|
67
|
+
});
|
|
36
68
|
// Stamped against the manifest this deploy was built from, so the watch loop
|
|
37
|
-
// leaves a tree that `juno verify-types` accepts.
|
|
69
|
+
// leaves a tree that `juno integration verify-types` accepts.
|
|
38
70
|
const written = await writeTypes(
|
|
39
71
|
stampTypeFiles(pickTypeFiles(result.types), await readManifestRaw(io.cwd)),
|
|
40
72
|
io.cwd
|
|
41
73
|
);
|
|
74
|
+
if (
|
|
75
|
+
!deploysElsewhere(project, argv) &&
|
|
76
|
+
project.baseVersion !== result.version
|
|
77
|
+
) {
|
|
78
|
+
const wrote = await updateIntegrationPackage(
|
|
79
|
+
io.cwd,
|
|
80
|
+
(pkg) => {
|
|
81
|
+
pkg.juno.base_version = result.version;
|
|
82
|
+
},
|
|
83
|
+
io
|
|
84
|
+
);
|
|
85
|
+
if (wrote) written.push('package.json');
|
|
86
|
+
}
|
|
87
|
+
if (argv.json) {
|
|
88
|
+
printJSON(result, io);
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
42
91
|
for (const warning of result.warnings ?? []) io.warn(`warning: ${warning}`);
|
|
43
92
|
io.log(
|
|
44
|
-
`deployed ${result.
|
|
93
|
+
`deployed ${result.id} v${result.version} to ${target} (${written.sort().join(', ')})`
|
|
45
94
|
);
|
|
46
95
|
return result;
|
|
47
96
|
}
|
|
48
97
|
|
|
49
98
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
99
|
+
* baseFor is the base a check or deploy sends: the copy's
|
|
100
|
+
* `juno.base_version`, unless --id names an install other than the
|
|
101
|
+
* manifest's. That install's versions are ones this copy has never seen, so
|
|
102
|
+
* there is nothing to compare against, and the warning says the check is off
|
|
103
|
+
* rather than letting it pass silently. A first deploy to a new id has nothing
|
|
104
|
+
* to be stale against either, so this needs no --force.
|
|
53
105
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
void runWatchIteration(client, argv, io, target);
|
|
64
|
-
}, WATCH_DEBOUNCE_MS);
|
|
65
|
-
};
|
|
106
|
+
export function baseFor(project, argv, io) {
|
|
107
|
+
if (deploysElsewhere(project, argv)) {
|
|
108
|
+
io.warn(
|
|
109
|
+
`no stale-base check: deploying to ${argv.id}, not ${project.manifest?.id}`
|
|
110
|
+
);
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
return project.baseVersion;
|
|
114
|
+
}
|
|
66
115
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
});
|
|
77
|
-
});
|
|
78
|
-
return 0;
|
|
116
|
+
/** Whether --id names an install other than the one the manifest does. */
|
|
117
|
+
function deploysElsewhere(project, argv) {
|
|
118
|
+
return argv.id !== undefined && argv.id !== project.manifest?.id;
|
|
79
119
|
}
|
|
80
120
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Watch mode is the loop the whole design is for: edit the manifest, the types
|
|
123
|
+
* regenerate, and the editor red-squiggles the method you have not written yet.
|
|
124
|
+
*/
|
|
125
|
+
function watchAndDeploy(client, argv, io, target) {
|
|
126
|
+
return deployOnChange({
|
|
127
|
+
io,
|
|
128
|
+
target,
|
|
129
|
+
paths: [
|
|
130
|
+
{ path: join(io.cwd, 'src'), recursive: true },
|
|
131
|
+
{ path: join(io.cwd, 'manifest.json') },
|
|
132
|
+
],
|
|
133
|
+
once: () => deployOnce(client, argv, io, target),
|
|
134
|
+
});
|
|
91
135
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { connect } from '../api.js';
|
|
2
|
+
import { DiagnosticsError } from '../errors.js';
|
|
3
|
+
import { readFlowProject } from '../flow-project.js';
|
|
4
|
+
import { runLocalChecks } 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
|
+
const failed = !argv['skip-local'] && (await runLocalChecks(argv, io));
|
|
19
|
+
warnOnDependencyDrift(project, io);
|
|
20
|
+
|
|
21
|
+
const { target, client } = await connect(argv, io);
|
|
22
|
+
let result;
|
|
23
|
+
try {
|
|
24
|
+
result = await client.ingestFlow(
|
|
25
|
+
{
|
|
26
|
+
flowId: project.flowId,
|
|
27
|
+
manifest: project.manifest,
|
|
28
|
+
files: project.files,
|
|
29
|
+
baseVersion: project.baseVersion,
|
|
30
|
+
},
|
|
31
|
+
{ dryRun: true }
|
|
32
|
+
);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (argv.json && err instanceof DiagnosticsError) {
|
|
35
|
+
return printDiagnosticsJSON(err, io);
|
|
36
|
+
}
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (argv.json) {
|
|
41
|
+
printJSON(result, io);
|
|
42
|
+
} else {
|
|
43
|
+
printWarnings(result.warnings, io);
|
|
44
|
+
io.log(`${result.flow_id}: manifest and src/ verified against ${target}`);
|
|
45
|
+
}
|
|
46
|
+
return failed ? 1 : 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* manifest.dependencies is what verify installs; package.json's dependencies
|
|
51
|
+
* are only there so a local `npm install` gives tsc the same modules. When the
|
|
52
|
+
* two disagree, local and server-side checks are checking different programs,
|
|
53
|
+
* so say which packages differ — and that the manifest is the one to edit.
|
|
54
|
+
*/
|
|
55
|
+
export function warnOnDependencyDrift(project, io) {
|
|
56
|
+
const declared = project.manifest?.dependencies ?? {};
|
|
57
|
+
const installed = project.packageJSON?.dependencies ?? {};
|
|
58
|
+
const names = [
|
|
59
|
+
...new Set([...Object.keys(declared), ...Object.keys(installed)]),
|
|
60
|
+
].sort();
|
|
61
|
+
const diffs = [];
|
|
62
|
+
for (const name of names) {
|
|
63
|
+
const a = declared[name];
|
|
64
|
+
const b = installed[name];
|
|
65
|
+
if (a === b) continue;
|
|
66
|
+
if (b === undefined) diffs.push(`${name} (only in manifest.json)`);
|
|
67
|
+
else if (a === undefined) diffs.push(`${name} (only in package.json)`);
|
|
68
|
+
else diffs.push(`${name} (manifest.json ${a}, package.json ${b})`);
|
|
69
|
+
}
|
|
70
|
+
if (diffs.length === 0) return;
|
|
71
|
+
io.warn(
|
|
72
|
+
`warning: manifest.json and package.json list different dependencies: ${diffs.join(', ')}. ` +
|
|
73
|
+
"Juno installs manifest.json's; add packages there, then copy them to package.json (or run `npm run pull` after deploying)."
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
MANIFEST,
|
|
12
|
+
manifestText,
|
|
13
|
+
readFlowProject,
|
|
14
|
+
SRC_DIR,
|
|
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 src/ 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 files under types/, and the new
|
|
33
|
+
* base_version. --watch redeploys on every save to src/ or manifest.json.
|
|
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: [
|
|
49
|
+
{ path: join(io.cwd, SRC_DIR), recursive: true },
|
|
50
|
+
{ path: join(io.cwd, MANIFEST) },
|
|
51
|
+
],
|
|
52
|
+
once: async () => {
|
|
53
|
+
const project = await readFlowProject(io.cwd);
|
|
54
|
+
const key = JSON.stringify([project.manifestRaw, project.files]);
|
|
55
|
+
if (key === last) return;
|
|
56
|
+
const after = await deployProject(client, project, argv, io, target);
|
|
57
|
+
last = JSON.stringify([after.manifestRaw, project.files]);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function deployOnce(client, argv, io, target) {
|
|
63
|
+
const project = await readFlowProject(io.cwd);
|
|
64
|
+
try {
|
|
65
|
+
await deployProject(client, project, argv, io, target);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (argv.json && err instanceof DiagnosticsError) {
|
|
68
|
+
return printDiagnosticsJSON(err, io);
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function deployProject(client, project, argv, io, target) {
|
|
76
|
+
const result = await client.ingestFlow({
|
|
77
|
+
flowId: project.flowId,
|
|
78
|
+
manifest: project.manifest,
|
|
79
|
+
files: project.files,
|
|
80
|
+
baseVersion: argv.force ? undefined : project.baseVersion,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const manifestRaw = manifestText(result.manifest);
|
|
84
|
+
const written = await writeFlowFiles(io.cwd, {
|
|
85
|
+
[MANIFEST]: manifestRaw,
|
|
86
|
+
...typeFiles(result.types),
|
|
87
|
+
});
|
|
88
|
+
if (project.baseVersion !== result.version) {
|
|
89
|
+
await updatePackageJSON(io.cwd, (pkg) => {
|
|
90
|
+
pkg.juno.base_version = result.version;
|
|
91
|
+
});
|
|
92
|
+
written.push('package.json');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (argv.json) {
|
|
96
|
+
printJSON(result, io);
|
|
97
|
+
} else {
|
|
98
|
+
printWarnings(result.warnings, io);
|
|
99
|
+
const files = written.length ? ` (${written.sort().join(', ')})` : '';
|
|
100
|
+
io.log(
|
|
101
|
+
`deployed ${result.flow_id} v${result.version} to ${target}${files}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return { manifestRaw };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** A route's warnings, each as a diagnostic line with a `warning: ` prefix. */
|
|
108
|
+
export function printWarnings(warnings, io) {
|
|
109
|
+
for (const w of warnings ?? []) io.warn(`warning: ${formatDiagnostic(w)}`);
|
|
110
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { connect } from '../api.js';
|
|
5
|
+
import { UsageError, UserError } from '../errors.js';
|
|
6
|
+
import { readFlowPackage } from '../flow-project.js';
|
|
7
|
+
import { baseVersionOf } from '../working-copy.js';
|
|
8
|
+
import { exists } from './init.js';
|
|
9
|
+
|
|
10
|
+
export const FLOW_FIXTURE_USAGE = 'juno flow fixture --run <seq> [--force]';
|
|
11
|
+
|
|
12
|
+
export const FIXTURES_DIR = 'test/fixtures';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `juno flow fixture --run <seq>` — write run `seq` of this working copy's
|
|
16
|
+
* flow as a flowHarness scenario, `test/fixtures/<flow-id>-<seq>.json`, for a
|
|
17
|
+
* test to replay with `h.scenario(fixture)`.
|
|
18
|
+
*
|
|
19
|
+
* The server builds the fixture (GET /api/flows/{id}/runs/{seq}/fixture) and
|
|
20
|
+
* the CLI writes what it is handed, as `init` does a scaffold. What the run
|
|
21
|
+
* did not record comes back as notes, one per line on stderr: each is
|
|
22
|
+
* something the author checks or fills in before the fixture replays.
|
|
23
|
+
*
|
|
24
|
+
* The run is a flag, not a positional, because a working-copy command takes
|
|
25
|
+
* none except `init`. It writes only a local file, so its effect is `read`.
|
|
26
|
+
*/
|
|
27
|
+
export async function flowFixture(argv, io) {
|
|
28
|
+
const seq = parseSeq(argv.run);
|
|
29
|
+
const pkg = await readFlowPackage(io.cwd);
|
|
30
|
+
const flowId = pkg.juno.flow_id;
|
|
31
|
+
|
|
32
|
+
const rel = `${FIXTURES_DIR}/${flowId}-${seq}.json`;
|
|
33
|
+
const path = join(io.cwd, rel);
|
|
34
|
+
// Refused before anything is fetched: an existing fixture may be one the
|
|
35
|
+
// author has edited (a credential filled in, a state corrected).
|
|
36
|
+
if (!argv.force && (await exists(path))) {
|
|
37
|
+
throw new UserError(`${rel} already exists; pass --force to overwrite it`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { client } = await connect(argv, io);
|
|
41
|
+
const { fixture, notes } = await client.runFixture(flowId, seq);
|
|
42
|
+
|
|
43
|
+
await mkdir(join(io.cwd, FIXTURES_DIR), { recursive: true });
|
|
44
|
+
await writeFile(path, `${JSON.stringify(fixture, null, 2)}\n`, 'utf8');
|
|
45
|
+
|
|
46
|
+
io.log(`wrote ${rel}`);
|
|
47
|
+
const base = baseVersionOf(pkg);
|
|
48
|
+
if (
|
|
49
|
+
base !== undefined &&
|
|
50
|
+
Number.isInteger(fixture?.flow_version) &&
|
|
51
|
+
fixture.flow_version !== base
|
|
52
|
+
) {
|
|
53
|
+
io.warn(
|
|
54
|
+
`run ${seq} ran v${fixture.flow_version}; this working copy is at v${base} — the fixture may not replay`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
for (const note of notes ?? []) io.warn(`note: ${note}`);
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** parseSeq is `--run`'s value as a run number, or a usage error. */
|
|
62
|
+
function parseSeq(value) {
|
|
63
|
+
if (value === undefined || !/^[1-9][0-9]*$/.test(value)) {
|
|
64
|
+
throw new UsageError(`usage: ${FLOW_FIXTURE_USAGE}`);
|
|
65
|
+
}
|
|
66
|
+
return Number(value);
|
|
67
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { connect } from '../api.js';
|
|
2
|
+
import { UserError } from '../errors.js';
|
|
3
|
+
import {
|
|
4
|
+
readFlowManifest,
|
|
5
|
+
readFlowPackage,
|
|
6
|
+
typeFiles,
|
|
7
|
+
writeFlowFiles,
|
|
8
|
+
} from '../flow-project.js';
|
|
9
|
+
|
|
10
|
+
export const FLOW_GEN_TYPES_USAGE = 'juno flow gen-types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `juno flow gen-types` — regenerate types/flow-env.d.ts and
|
|
14
|
+
* types/flow-api.d.ts from manifest.json against the instance's live catalog.
|
|
15
|
+
* manifest.json itself is left alone: this refreshes what the editor
|
|
16
|
+
* type-checks against, nothing more. Stale types cannot ship regardless,
|
|
17
|
+
* because ingest regenerates them.
|
|
18
|
+
*/
|
|
19
|
+
export async function flowGenTypes(argv, io) {
|
|
20
|
+
await readFlowPackage(io.cwd);
|
|
21
|
+
const { manifest } = await readFlowManifest(io.cwd);
|
|
22
|
+
const { client } = await connect(argv, io);
|
|
23
|
+
const generated = await client.typegenFlow(manifest);
|
|
24
|
+
const files = typeFiles(generated.types);
|
|
25
|
+
if (!Object.keys(files).length) {
|
|
26
|
+
throw new UserError(
|
|
27
|
+
`${client.target} answered no .d.ts files for this manifest`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const written = await writeFlowFiles(io.cwd, files);
|
|
31
|
+
io.log(
|
|
32
|
+
written.length
|
|
33
|
+
? `wrote ${written.join(', ')}`
|
|
34
|
+
: `${Object.keys(files).sort().join(', ')} already up to date`
|
|
35
|
+
);
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { connect } from '../api.js';
|
|
5
|
+
import { printAuthoringIndex } from '../authoring-skills.js';
|
|
6
|
+
import { UsageError, UserError } from '../errors.js';
|
|
7
|
+
import { exists, safeRelative } from './init.js';
|
|
8
|
+
|
|
9
|
+
export const FLOW_INIT_USAGE = 'juno flow init <id> [--here]';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `juno flow init <id>` — a working copy of the flow's active version, or a
|
|
13
|
+
* stub for an id with none, written into ./<id> (or the current directory
|
|
14
|
+
* with --here).
|
|
15
|
+
*
|
|
16
|
+
* Every file comes from POST /api/flows/scaffold, the single generator of a
|
|
17
|
+
* flow working copy: the CLI writes what it is handed and holds no copy of
|
|
18
|
+
* the layout, the script lines or the types. After the next steps it prints
|
|
19
|
+
* the flow author's skill index, which is where an external agent starts.
|
|
20
|
+
*/
|
|
21
|
+
export async function flowInit(argv, io) {
|
|
22
|
+
const [id, ...extra] = argv._;
|
|
23
|
+
if (!id || extra.length > 0) {
|
|
24
|
+
throw new UsageError(`usage: ${FLOW_INIT_USAGE}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const dir = resolve(io.cwd, argv.here ? '.' : id);
|
|
28
|
+
if (!argv.here && (await exists(dir))) {
|
|
29
|
+
throw new UserError(`${dir} already exists`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { target, client } = await connect(argv, io);
|
|
33
|
+
// The id is validated server-side: one answer to "is this a valid flow id".
|
|
34
|
+
const { files, warnings } = await client.scaffoldFlow(id, { target });
|
|
35
|
+
|
|
36
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
37
|
+
const path = join(dir, safeRelative(rel));
|
|
38
|
+
await mkdir(dirname(path), { recursive: true });
|
|
39
|
+
await writeFile(path, content, 'utf8');
|
|
40
|
+
}
|
|
41
|
+
printScaffoldWarnings(warnings, io);
|
|
42
|
+
|
|
43
|
+
io.log(`created ${dir}`);
|
|
44
|
+
io.log('');
|
|
45
|
+
io.log(argv.here ? 'npm install' : `cd ${id} && npm install`);
|
|
46
|
+
io.log('npm test');
|
|
47
|
+
io.log('npm run deploy');
|
|
48
|
+
await printAuthoringIndex('flow', client, target, io);
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A stored manifest the live catalog now refuses (a state it binds was
|
|
54
|
+
* deleted, say) still scaffolds, because the working copy is how it gets
|
|
55
|
+
* fixed; the refusal comes along as warnings, and they go to stderr.
|
|
56
|
+
*/
|
|
57
|
+
export function printScaffoldWarnings(warnings, io) {
|
|
58
|
+
for (const w of warnings ?? []) io.warn(`warning: ${w}`);
|
|
59
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { connect } from '../api.js';
|
|
2
|
+
import { UserError } from '../errors.js';
|
|
3
|
+
import {
|
|
4
|
+
MANIFEST,
|
|
5
|
+
manifestText,
|
|
6
|
+
PACKAGE_JSON,
|
|
7
|
+
readFlowPackage,
|
|
8
|
+
updatePackageJSON,
|
|
9
|
+
writeFlowFiles,
|
|
10
|
+
} from '../flow-project.js';
|
|
11
|
+
import { printScaffoldWarnings } from './flow-init.js';
|
|
12
|
+
import {
|
|
13
|
+
baseArtifactPaths,
|
|
14
|
+
baseVersionOf,
|
|
15
|
+
pullArtifacts,
|
|
16
|
+
requireCleanTree,
|
|
17
|
+
setDependencies,
|
|
18
|
+
} from '../working-copy.js';
|
|
19
|
+
|
|
20
|
+
export const FLOW_PULL_USAGE = 'juno flow pull [--force]';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `juno flow pull` — make the working copy's artifact files the flow's active
|
|
24
|
+
* version: manifest.json and every file under src/ and test/, exactly as the
|
|
25
|
+
* version stores them (GET /api/flows/{id}/versions/{v}), never as the
|
|
26
|
+
* scaffold renders them. types/ is generated, so it comes from
|
|
27
|
+
* POST /api/flows/scaffold, which also names the active version. In
|
|
28
|
+
* package.json only `dependencies` (the manifest's) and `juno.base_version`
|
|
29
|
+
* are Juno's; `scripts` and every other key are the author's and stay.
|
|
30
|
+
*
|
|
31
|
+
* A local file is deleted only when the base version (`juno.base_version`)
|
|
32
|
+
* had it and the pulled one does not; a file neither has is one not deployed
|
|
33
|
+
* yet, and stays. Project files (tsconfig.json, README.md …) are never
|
|
34
|
+
* written or deleted.
|
|
35
|
+
*
|
|
36
|
+
* It refuses a dirty git tree without --force, as integration pull does: an
|
|
37
|
+
* uncommitted edit is the one thing nothing can get back.
|
|
38
|
+
*/
|
|
39
|
+
export async function flowPull(argv, io) {
|
|
40
|
+
const pkg = await readFlowPackage(io.cwd);
|
|
41
|
+
const flowId = pkg.juno.flow_id;
|
|
42
|
+
|
|
43
|
+
await requireCleanTree(io.cwd, {
|
|
44
|
+
force: argv.force,
|
|
45
|
+
overwrites: `${MANIFEST}, src/, test/ and types/`,
|
|
46
|
+
io,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const { target, client } = await connect(argv, io);
|
|
50
|
+
const { files, warnings } = await client.scaffoldFlow(flowId, { target });
|
|
51
|
+
let scaffoldPkg;
|
|
52
|
+
try {
|
|
53
|
+
scaffoldPkg = JSON.parse(files[PACKAGE_JSON]);
|
|
54
|
+
} catch {
|
|
55
|
+
throw new UserError(
|
|
56
|
+
`${target} answered a working copy with no readable ${PACKAGE_JSON}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const version = scaffoldPkg.juno?.base_version;
|
|
60
|
+
const noActive = () =>
|
|
61
|
+
new UserError(
|
|
62
|
+
`${flowId} has no active version on ${target} — nothing to pull`
|
|
63
|
+
);
|
|
64
|
+
// No base_version is the scaffold's stub for a flow with no active
|
|
65
|
+
// version; writing it over the author's work would lose it for nothing.
|
|
66
|
+
if (!Number.isInteger(version)) throw noActive();
|
|
67
|
+
|
|
68
|
+
// Everything is fetched before anything is written, so a failure leaves
|
|
69
|
+
// the working copy as it was.
|
|
70
|
+
const detail = await client.getFlowVersion(flowId, version);
|
|
71
|
+
if (!detail) throw noActive();
|
|
72
|
+
const manifest = JSON.parse(detail.manifest_json);
|
|
73
|
+
const stored = {
|
|
74
|
+
[MANIFEST]: manifestText(manifest),
|
|
75
|
+
...detail.source_files,
|
|
76
|
+
};
|
|
77
|
+
const base = await baseArtifactPaths({
|
|
78
|
+
base: baseVersionOf(pkg),
|
|
79
|
+
version,
|
|
80
|
+
stored,
|
|
81
|
+
read: async (v) => {
|
|
82
|
+
const b = await client.getFlowVersion(flowId, v);
|
|
83
|
+
return b ? [MANIFEST, ...Object.keys(b.source_files ?? {})] : null;
|
|
84
|
+
},
|
|
85
|
+
id: flowId,
|
|
86
|
+
target,
|
|
87
|
+
io,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const { written, removed } = await pullArtifacts(io.cwd, {
|
|
91
|
+
stored,
|
|
92
|
+
base,
|
|
93
|
+
kind: 'flow',
|
|
94
|
+
});
|
|
95
|
+
written.push(
|
|
96
|
+
...(await writeFlowFiles(
|
|
97
|
+
io.cwd,
|
|
98
|
+
Object.fromEntries(
|
|
99
|
+
Object.entries(files).filter(([name]) => name.startsWith('types/'))
|
|
100
|
+
)
|
|
101
|
+
))
|
|
102
|
+
);
|
|
103
|
+
await updatePackageJSON(io.cwd, (p) => {
|
|
104
|
+
setDependencies(p, manifest);
|
|
105
|
+
p.juno.base_version = version;
|
|
106
|
+
});
|
|
107
|
+
written.push(PACKAGE_JSON);
|
|
108
|
+
printScaffoldWarnings(warnings, io);
|
|
109
|
+
|
|
110
|
+
io.log(
|
|
111
|
+
`pulled ${flowId} v${version} from ${target} (${written.sort().join(', ')})`
|
|
112
|
+
);
|
|
113
|
+
if (removed.length) io.log(`removed ${removed.join(', ')}`);
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
import { JunoClient } from '../api.js';
|
|
1
|
+
import { clientName, JunoClient } from '../api.js';
|
|
2
2
|
import { readManifest, readManifestRaw, writeTypes } from '../project.js';
|
|
3
3
|
import { stampTypeFiles } from '../stamp.js';
|
|
4
4
|
import { resolveTarget } from '../target.js';
|
|
5
5
|
|
|
6
|
+
export const GEN_TYPES_USAGE = 'juno integration gen-types';
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
|
-
* `juno types` — POST manifest.json, write back types/.
|
|
9
|
+
* `juno integration gen-types` — POST manifest.json, write back types/.
|
|
8
10
|
*
|
|
9
11
|
* Both halves come from one call so they cannot skew, and they are committed:
|
|
10
12
|
* a fresh clone compiles offline, and a contributor fixing a bug in someone
|
|
11
13
|
* else's integration never needs a Juno instance. Only the person editing the
|
|
12
14
|
* manifest does, which is the right person to require it of.
|
|
13
15
|
*/
|
|
14
|
-
export async function
|
|
15
|
-
const target = await resolveTarget({
|
|
16
|
+
export async function genTypes(argv, io) {
|
|
17
|
+
const target = await resolveTarget({
|
|
18
|
+
url: argv.url,
|
|
19
|
+
cwd: io.cwd,
|
|
20
|
+
env: io.env,
|
|
21
|
+
});
|
|
16
22
|
const raw = await readManifestRaw(io.cwd);
|
|
17
23
|
const manifest = await readManifest(io.cwd);
|
|
18
|
-
const client =
|
|
24
|
+
const client =
|
|
25
|
+
argv.client ?? new JunoClient(target, { client: clientName(io.env) });
|
|
19
26
|
|
|
20
27
|
const generated = await client.typegen(manifest);
|
|
21
28
|
const written = await writeTypes(
|