@junoflow/cli 0.6.0 → 0.10.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 +214 -80
- package/app-client.js +306 -0
- package/eslint.app.config.js +34 -0
- package/eslint.flow.config.js +4 -2
- package/package.json +9 -4
- package/src/api.js +213 -51
- 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 +2 -1
- package/src/commands/app-check.js +75 -0
- package/src/commands/app-deploy.js +113 -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/check.js +33 -24
- package/src/commands/config.js +26 -7
- package/src/commands/deploy.js +59 -13
- package/src/commands/flow-check.js +4 -22
- package/src/commands/flow-deploy.js +11 -8
- package/src/commands/flow-fixture.js +67 -0
- package/src/commands/flow-gen-types.js +12 -5
- package/src/commands/flow-pull.js +69 -40
- package/src/commands/gen-types.js +2 -9
- package/src/commands/init.js +2 -9
- package/src/commands/pull.js +98 -111
- package/src/config.js +20 -5
- package/src/errors.js +3 -3
- package/src/exit.js +2 -2
- package/src/flow-project.js +47 -97
- package/src/index.js +99 -23
- package/src/project.js +76 -8
- package/src/token.js +30 -0
- package/src/watch.js +48 -11
- package/src/working-copy.js +262 -0
- package/tsconfig.page.json +15 -0
- package/verbs.json +52 -3
package/src/commands/check.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { connect } from '../api.js';
|
|
5
5
|
import { printDiagnosticsJSON, printJSON } from '../diagnostics.js';
|
|
6
6
|
import { DiagnosticsError, UserError } from '../errors.js';
|
|
7
7
|
import { readProject } from '../project.js';
|
|
8
|
-
import {
|
|
8
|
+
import { baseFor } from './deploy.js';
|
|
9
9
|
import { verifyTypes } from './verify-types.js';
|
|
10
10
|
|
|
11
11
|
export const CHECK_USAGE =
|
|
@@ -17,7 +17,8 @@ export const CHECK_USAGE =
|
|
|
17
17
|
* tsc and eslint run locally against the committed types; reserved names,
|
|
18
18
|
* cross-surface collisions and JSON Schema compilation need the server, so
|
|
19
19
|
* they go through ingest?dry_run=1, which stores nothing and is safe to point
|
|
20
|
-
* at a production instance.
|
|
20
|
+
* at a production instance. It sends `juno.base_version` like deploy does, so
|
|
21
|
+
* a stale copy is a 409 (exit 4) here, before deploy hits it.
|
|
21
22
|
*
|
|
22
23
|
* --json prints the route's body (or the 422's diagnostics) on stdout and
|
|
23
24
|
* nothing else; the local tools' own output goes to stderr.
|
|
@@ -36,32 +37,18 @@ export async function check(argv, io) {
|
|
|
36
37
|
io.error(err.message);
|
|
37
38
|
failed = true;
|
|
38
39
|
}
|
|
39
|
-
|
|
40
|
-
['typecheck', 'tsc', ['--noEmit']],
|
|
41
|
-
['lint', 'eslint', ['.']],
|
|
42
|
-
]) {
|
|
43
|
-
const code = await runLocal(bin, args, io, {
|
|
44
|
-
quietStdout: argv.json,
|
|
45
|
-
});
|
|
46
|
-
if (code === SKIPPED) {
|
|
47
|
-
io.warn(`${label}: ${bin} is not installed — run npm install`);
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (code !== 0) failed = true;
|
|
51
|
-
}
|
|
40
|
+
if (await runLocalChecks(argv, io)) failed = true;
|
|
52
41
|
}
|
|
53
42
|
|
|
54
|
-
const target = await
|
|
55
|
-
url: argv.url,
|
|
56
|
-
cwd: io.cwd,
|
|
57
|
-
env: io.env,
|
|
58
|
-
});
|
|
59
|
-
const client =
|
|
60
|
-
argv.client ?? new JunoClient(target, { client: clientName(io.env) });
|
|
43
|
+
const { target, client } = await connect(argv, io);
|
|
61
44
|
const project = await readProject(io.cwd);
|
|
62
45
|
let result;
|
|
63
46
|
try {
|
|
64
|
-
result = await client.ingest(project, {
|
|
47
|
+
result = await client.ingest(project, {
|
|
48
|
+
dryRun: true,
|
|
49
|
+
id: argv.id,
|
|
50
|
+
baseVersion: baseFor(project, argv, io),
|
|
51
|
+
});
|
|
65
52
|
} catch (err) {
|
|
66
53
|
if (argv.json && err instanceof DiagnosticsError) {
|
|
67
54
|
return printDiagnosticsJSON(err, io);
|
|
@@ -81,6 +68,28 @@ export async function check(argv, io) {
|
|
|
81
68
|
|
|
82
69
|
export const SKIPPED = -1;
|
|
83
70
|
|
|
71
|
+
/**
|
|
72
|
+
* runLocalChecks runs tsc and eslint from the working copy's node_modules,
|
|
73
|
+
* the offline half of every kind's `check`, and answers whether either
|
|
74
|
+
* failed. A tool that is not installed is a warning, not a failure: the
|
|
75
|
+
* server-side half still runs.
|
|
76
|
+
*/
|
|
77
|
+
export async function runLocalChecks(argv, io) {
|
|
78
|
+
let failed = false;
|
|
79
|
+
for (const [label, bin, args] of [
|
|
80
|
+
['typecheck', 'tsc', ['--noEmit']],
|
|
81
|
+
['lint', 'eslint', ['.']],
|
|
82
|
+
]) {
|
|
83
|
+
const code = await runLocal(bin, args, io, { quietStdout: argv.json });
|
|
84
|
+
if (code === SKIPPED) {
|
|
85
|
+
io.warn(`${label}: ${bin} is not installed — run npm install`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (code !== 0) failed = true;
|
|
89
|
+
}
|
|
90
|
+
return failed;
|
|
91
|
+
}
|
|
92
|
+
|
|
84
93
|
/**
|
|
85
94
|
* Runs a local devtool from node_modules/.bin. Its output goes straight to the
|
|
86
95
|
* terminal: tsc and eslint already print file:line:col, and reformatting them
|
package/src/commands/config.js
CHANGED
|
@@ -2,25 +2,44 @@ import { readUserConfig, writeUserConfig } from '../config.js';
|
|
|
2
2
|
import { UsageError } from '../errors.js';
|
|
3
3
|
import { normalizeTarget } from '../target.js';
|
|
4
4
|
|
|
5
|
-
export const CONFIG_SET_USAGE =
|
|
5
|
+
export const CONFIG_SET_USAGE =
|
|
6
|
+
'juno config set url <url> | juno config set token <key>';
|
|
7
|
+
|
|
8
|
+
const KEYS = ['url', 'token'];
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
|
-
* `juno config set url <url>`
|
|
9
|
-
*
|
|
10
|
-
* other source is, so what lands in the file is
|
|
11
|
+
* `juno config set url <url>` and `juno config set token <key>` write the
|
|
12
|
+
* user config file, the last place resolveTarget and resolveToken look. A url
|
|
13
|
+
* is normalised the way every other source is, so what lands in the file is
|
|
14
|
+
* what a request would use. A token must start `juno_`, the prefix of every
|
|
15
|
+
* API key, which catches a pasted password or a key's id alone; the file is
|
|
16
|
+
* written 0600, and neither the token nor any part of it is echoed, the
|
|
17
|
+
* refusal included.
|
|
11
18
|
*/
|
|
12
19
|
export async function configSet(argv, io) {
|
|
13
20
|
const [key, value, ...extra] = argv._;
|
|
14
21
|
const usage = `usage: ${CONFIG_SET_USAGE}`;
|
|
15
22
|
if (key === undefined) throw new UsageError(usage);
|
|
16
|
-
if (key
|
|
23
|
+
if (!KEYS.includes(key)) {
|
|
17
24
|
throw new UsageError(`unknown config key "${key}"; ${usage}`);
|
|
18
25
|
}
|
|
19
26
|
if (value === undefined || extra.length) throw new UsageError(usage);
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
let entry;
|
|
29
|
+
if (key === 'url') {
|
|
30
|
+
entry = { url: normalizeTarget(value) };
|
|
31
|
+
} else {
|
|
32
|
+
const token = value.trim();
|
|
33
|
+
if (!token.startsWith('juno_')) {
|
|
34
|
+
throw new UsageError(
|
|
35
|
+
'that is not an API key: a key starts with "juno_". Create one under ' +
|
|
36
|
+
'Settings → API keys.'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
entry = { token };
|
|
40
|
+
}
|
|
22
41
|
const current = await readUserConfig(io.env);
|
|
23
|
-
const path = await writeUserConfig(io.env, { ...current,
|
|
42
|
+
const path = await writeUserConfig(io.env, { ...current, ...entry });
|
|
24
43
|
io.warn(`wrote ${path}`);
|
|
25
44
|
return 0;
|
|
26
45
|
}
|
package/src/commands/deploy.js
CHANGED
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { connect } from '../api.js';
|
|
4
4
|
import { printDiagnosticsJSON, printJSON } from '../diagnostics.js';
|
|
5
5
|
import { DiagnosticsError } from '../errors.js';
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
readManifestRaw,
|
|
8
|
+
readProject,
|
|
9
|
+
updateIntegrationPackage,
|
|
10
|
+
writeTypes,
|
|
11
|
+
} from '../project.js';
|
|
8
12
|
import { stampTypeFiles } from '../stamp.js';
|
|
9
13
|
import { pickTypeFiles } from './gen-types.js';
|
|
10
14
|
import { deployOnChange } from '../watch.js';
|
|
11
15
|
|
|
12
16
|
export const DEPLOY_USAGE =
|
|
13
|
-
'juno integration deploy [--watch] [--id <id>] [--json]';
|
|
17
|
+
'juno integration deploy [--watch] [--force] [--id <id>] [--json]';
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* `juno integration deploy [--watch]` — upload the working copy, verify, activate.
|
|
17
21
|
*
|
|
22
|
+
* It sends `juno.base_version`, so an install changed elsewhere since this
|
|
23
|
+
* copy was pulled is a 409 (exit 4) rather than a silent overwrite; --force
|
|
24
|
+
* sends none and replaces whatever is active. A successful deploy writes the
|
|
25
|
+
* new version back as the base. --id naming another install sends no base and
|
|
26
|
+
* writes none back: the base belongs to the install the manifest names (see
|
|
27
|
+
* baseFor).
|
|
28
|
+
*
|
|
18
29
|
* The response carries the regenerated types, so a deploy keeps the committed
|
|
19
30
|
* types/ honest without a second round trip that could answer for a different
|
|
20
31
|
* manifest.
|
|
@@ -23,13 +34,7 @@ export const DEPLOY_USAGE =
|
|
|
23
34
|
* of the summary line, for CI.
|
|
24
35
|
*/
|
|
25
36
|
export async function deploy(argv, io) {
|
|
26
|
-
const target = await
|
|
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) });
|
|
37
|
+
const { target, client } = await connect(argv, io);
|
|
33
38
|
|
|
34
39
|
if (!argv.watch) {
|
|
35
40
|
try {
|
|
@@ -46,25 +51,66 @@ export async function deploy(argv, io) {
|
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
async function deployOnce(client, argv, io, target) {
|
|
54
|
+
// Re-read on every deploy, base included, so the watch loop is checked
|
|
55
|
+
// against its own previous deploy rather than refused by it.
|
|
49
56
|
const project = await readProject(io.cwd);
|
|
50
|
-
const result = await client.ingest(project, {
|
|
57
|
+
const result = await client.ingest(project, {
|
|
58
|
+
id: argv.id,
|
|
59
|
+
baseVersion: argv.force ? undefined : baseFor(project, argv, io),
|
|
60
|
+
});
|
|
51
61
|
// Stamped against the manifest this deploy was built from, so the watch loop
|
|
52
62
|
// leaves a tree that `juno integration verify-types` accepts.
|
|
53
63
|
const written = await writeTypes(
|
|
54
64
|
stampTypeFiles(pickTypeFiles(result.types), await readManifestRaw(io.cwd)),
|
|
55
65
|
io.cwd
|
|
56
66
|
);
|
|
67
|
+
if (
|
|
68
|
+
!deploysElsewhere(project, argv) &&
|
|
69
|
+
project.baseVersion !== result.version
|
|
70
|
+
) {
|
|
71
|
+
const wrote = await updateIntegrationPackage(
|
|
72
|
+
io.cwd,
|
|
73
|
+
(pkg) => {
|
|
74
|
+
pkg.juno.base_version = result.version;
|
|
75
|
+
},
|
|
76
|
+
io
|
|
77
|
+
);
|
|
78
|
+
if (wrote) written.push('package.json');
|
|
79
|
+
}
|
|
57
80
|
if (argv.json) {
|
|
58
81
|
printJSON(result, io);
|
|
59
82
|
return result;
|
|
60
83
|
}
|
|
61
84
|
for (const warning of result.warnings ?? []) io.warn(`warning: ${warning}`);
|
|
62
85
|
io.log(
|
|
63
|
-
`deployed ${result.id} v${result.version} to ${target} (${written.join(', ')})`
|
|
86
|
+
`deployed ${result.id} v${result.version} to ${target} (${written.sort().join(', ')})`
|
|
64
87
|
);
|
|
65
88
|
return result;
|
|
66
89
|
}
|
|
67
90
|
|
|
91
|
+
/**
|
|
92
|
+
* baseFor is the base a check or deploy sends: the copy's
|
|
93
|
+
* `juno.base_version`, unless --id names an install other than the
|
|
94
|
+
* manifest's. That install's versions are ones this copy has never seen, so
|
|
95
|
+
* there is nothing to compare against, and the warning says the check is off
|
|
96
|
+
* rather than letting it pass silently. A first deploy to a new id has nothing
|
|
97
|
+
* to be stale against either, so this needs no --force.
|
|
98
|
+
*/
|
|
99
|
+
export function baseFor(project, argv, io) {
|
|
100
|
+
if (deploysElsewhere(project, argv)) {
|
|
101
|
+
io.warn(
|
|
102
|
+
`no stale-base check: deploying to ${argv.id}, not ${project.manifest?.id}`
|
|
103
|
+
);
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return project.baseVersion;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Whether --id names an install other than the one the manifest does. */
|
|
110
|
+
function deploysElsewhere(project, argv) {
|
|
111
|
+
return argv.id !== undefined && argv.id !== project.manifest?.id;
|
|
112
|
+
}
|
|
113
|
+
|
|
68
114
|
/**
|
|
69
115
|
* Watch mode is the loop the whole design is for: edit the manifest, the types
|
|
70
116
|
* regenerate, and the editor red-squiggles the method you have not written yet.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { connect } from '../api.js';
|
|
2
2
|
import { DiagnosticsError } from '../errors.js';
|
|
3
3
|
import { readFlowProject } from '../flow-project.js';
|
|
4
|
-
import {
|
|
4
|
+
import { runLocalChecks } from './check.js';
|
|
5
5
|
import { printDiagnosticsJSON, printJSON } from '../diagnostics.js';
|
|
6
6
|
import { printWarnings } from './flow-deploy.js';
|
|
7
7
|
|
|
@@ -15,23 +15,7 @@ export const FLOW_CHECK_USAGE = 'juno flow check [--skip-local] [--json]';
|
|
|
15
15
|
*/
|
|
16
16
|
export async function flowCheck(argv, io) {
|
|
17
17
|
const project = await readFlowProject(io.cwd);
|
|
18
|
-
|
|
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
|
-
}
|
|
18
|
+
const failed = !argv['skip-local'] && (await runLocalChecks(argv, io));
|
|
35
19
|
warnOnDependencyDrift(project, io);
|
|
36
20
|
|
|
37
21
|
const { target, client } = await connect(argv, io);
|
|
@@ -41,7 +25,7 @@ export async function flowCheck(argv, io) {
|
|
|
41
25
|
{
|
|
42
26
|
flowId: project.flowId,
|
|
43
27
|
manifest: project.manifest,
|
|
44
|
-
|
|
28
|
+
files: project.files,
|
|
45
29
|
baseVersion: project.baseVersion,
|
|
46
30
|
},
|
|
47
31
|
{ dryRun: true }
|
|
@@ -57,9 +41,7 @@ export async function flowCheck(argv, io) {
|
|
|
57
41
|
printJSON(result, io);
|
|
58
42
|
} else {
|
|
59
43
|
printWarnings(result.warnings, io);
|
|
60
|
-
io.log(
|
|
61
|
-
`${result.flow_id}: manifest and flow.ts verified against ${target}`
|
|
62
|
-
);
|
|
44
|
+
io.log(`${result.flow_id}: manifest and src/ verified against ${target}`);
|
|
63
45
|
}
|
|
64
46
|
return failed ? 1 : 0;
|
|
65
47
|
}
|
|
@@ -8,10 +8,10 @@ import {
|
|
|
8
8
|
printJSON,
|
|
9
9
|
} from '../diagnostics.js';
|
|
10
10
|
import {
|
|
11
|
-
FLOW_TS,
|
|
12
11
|
MANIFEST,
|
|
13
12
|
manifestText,
|
|
14
13
|
readFlowProject,
|
|
14
|
+
SRC_DIR,
|
|
15
15
|
typeFiles,
|
|
16
16
|
updatePackageJSON,
|
|
17
17
|
writeFlowFiles,
|
|
@@ -22,15 +22,15 @@ export const FLOW_DEPLOY_USAGE =
|
|
|
22
22
|
'juno flow deploy [--watch] [--force] [--json]';
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* `juno flow deploy` — upload manifest.json and
|
|
25
|
+
* `juno flow deploy` — upload manifest.json and src/ to
|
|
26
26
|
* POST /api/flows/ingest, which verifies and commits them through the same
|
|
27
27
|
* core the in-app assistant's submit_code uses, and activates the version.
|
|
28
28
|
*
|
|
29
29
|
* It sends `juno.base_version`, so a flow edited elsewhere since this copy was
|
|
30
30
|
* pulled is a 409 (exit 4) rather than a silent overwrite; --force sends none
|
|
31
31
|
* and replaces whatever is active. On success it writes back what the server
|
|
32
|
-
* now holds: the canonical manifest, both
|
|
33
|
-
* base_version.
|
|
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
34
|
*/
|
|
35
35
|
export async function flowDeploy(argv, io) {
|
|
36
36
|
const { target, client } = await connect(argv, io);
|
|
@@ -45,13 +45,16 @@ export async function flowDeploy(argv, io) {
|
|
|
45
45
|
return deployOnChange({
|
|
46
46
|
io,
|
|
47
47
|
target,
|
|
48
|
-
paths: [
|
|
48
|
+
paths: [
|
|
49
|
+
{ path: join(io.cwd, SRC_DIR), recursive: true },
|
|
50
|
+
{ path: join(io.cwd, MANIFEST) },
|
|
51
|
+
],
|
|
49
52
|
once: async () => {
|
|
50
53
|
const project = await readFlowProject(io.cwd);
|
|
51
|
-
const key = JSON.stringify([project.manifestRaw, project.
|
|
54
|
+
const key = JSON.stringify([project.manifestRaw, project.files]);
|
|
52
55
|
if (key === last) return;
|
|
53
56
|
const after = await deployProject(client, project, argv, io, target);
|
|
54
|
-
last = JSON.stringify([after.manifestRaw, project.
|
|
57
|
+
last = JSON.stringify([after.manifestRaw, project.files]);
|
|
55
58
|
},
|
|
56
59
|
});
|
|
57
60
|
}
|
|
@@ -73,7 +76,7 @@ async function deployProject(client, project, argv, io, target) {
|
|
|
73
76
|
const result = await client.ingestFlow({
|
|
74
77
|
flowId: project.flowId,
|
|
75
78
|
manifest: project.manifest,
|
|
76
|
-
|
|
79
|
+
files: project.files,
|
|
77
80
|
baseVersion: argv.force ? undefined : project.baseVersion,
|
|
78
81
|
});
|
|
79
82
|
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { connect } from '../api.js';
|
|
2
|
+
import { UserError } from '../errors.js';
|
|
2
3
|
import {
|
|
3
4
|
readFlowManifest,
|
|
4
5
|
readFlowPackage,
|
|
@@ -9,17 +10,23 @@ import {
|
|
|
9
10
|
export const FLOW_GEN_TYPES_USAGE = 'juno flow gen-types';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* `juno flow gen-types` — regenerate flow-env.d.ts and
|
|
13
|
-
* manifest.json against the instance's live catalog.
|
|
14
|
-
* left alone: this refreshes what the editor
|
|
15
|
-
* more. Stale types cannot ship regardless,
|
|
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.
|
|
16
18
|
*/
|
|
17
19
|
export async function flowGenTypes(argv, io) {
|
|
18
20
|
await readFlowPackage(io.cwd);
|
|
19
21
|
const { manifest } = await readFlowManifest(io.cwd);
|
|
20
22
|
const { client } = await connect(argv, io);
|
|
21
23
|
const generated = await client.typegenFlow(manifest);
|
|
22
|
-
const files = typeFiles(generated
|
|
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
|
+
}
|
|
23
30
|
const written = await writeFlowFiles(io.cwd, files);
|
|
24
31
|
io.log(
|
|
25
32
|
written.length
|
|
@@ -1,46 +1,50 @@
|
|
|
1
1
|
import { connect } from '../api.js';
|
|
2
2
|
import { UserError } from '../errors.js';
|
|
3
3
|
import {
|
|
4
|
-
FLOW_TS,
|
|
5
4
|
MANIFEST,
|
|
5
|
+
manifestText,
|
|
6
6
|
PACKAGE_JSON,
|
|
7
7
|
readFlowPackage,
|
|
8
8
|
updatePackageJSON,
|
|
9
9
|
writeFlowFiles,
|
|
10
10
|
} from '../flow-project.js';
|
|
11
11
|
import { printScaffoldWarnings } from './flow-init.js';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
baseArtifactPaths,
|
|
14
|
+
baseVersionOf,
|
|
15
|
+
pullArtifacts,
|
|
16
|
+
requireCleanTree,
|
|
17
|
+
setDependencies,
|
|
18
|
+
} from '../working-copy.js';
|
|
13
19
|
|
|
14
20
|
export const FLOW_PULL_USAGE = 'juno flow pull [--force]';
|
|
15
21
|
|
|
16
22
|
/**
|
|
17
|
-
* `juno flow pull` —
|
|
18
|
-
* version: manifest.json
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* POST /api/flows/scaffold,
|
|
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.
|
|
23
35
|
*
|
|
24
36
|
* It refuses a dirty git tree without --force, as integration pull does: an
|
|
25
37
|
* uncommitted edit is the one thing nothing can get back.
|
|
26
38
|
*/
|
|
27
39
|
export async function flowPull(argv, io) {
|
|
28
|
-
const
|
|
29
|
-
const flowId = juno.flow_id;
|
|
40
|
+
const pkg = await readFlowPackage(io.cwd);
|
|
41
|
+
const flowId = pkg.juno.flow_id;
|
|
30
42
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
);
|
|
37
|
-
} else if (dirty.length > 0) {
|
|
38
|
-
throw new UserError(
|
|
39
|
-
`working tree has uncommitted changes:\n${dirty.map((p) => ` ${p}`).join('\n')}\n` +
|
|
40
|
-
'Commit or stash them, or pass --force to overwrite.'
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
43
|
+
await requireCleanTree(io.cwd, {
|
|
44
|
+
force: argv.force,
|
|
45
|
+
overwrites: `${MANIFEST}, src/, test/ and types/`,
|
|
46
|
+
io,
|
|
47
|
+
});
|
|
44
48
|
|
|
45
49
|
const { target, client } = await connect(argv, io);
|
|
46
50
|
const { files, warnings } = await client.scaffoldFlow(flowId, { target });
|
|
@@ -53,28 +57,52 @@ export async function flowPull(argv, io) {
|
|
|
53
57
|
);
|
|
54
58
|
}
|
|
55
59
|
const version = scaffoldPkg.juno?.base_version;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (!Number.isInteger(version)) {
|
|
59
|
-
throw new UserError(
|
|
60
|
+
const noActive = () =>
|
|
61
|
+
new UserError(
|
|
60
62
|
`${flowId} has no active version on ${target} — nothing to pull`
|
|
61
63
|
);
|
|
62
|
-
|
|
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();
|
|
63
67
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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/'))
|
|
70
100
|
)
|
|
71
|
-
)
|
|
101
|
+
))
|
|
72
102
|
);
|
|
73
|
-
await updatePackageJSON(io.cwd, (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
else delete pkg.dependencies;
|
|
77
|
-
pkg.juno.base_version = version;
|
|
103
|
+
await updatePackageJSON(io.cwd, (p) => {
|
|
104
|
+
setDependencies(p, manifest);
|
|
105
|
+
p.juno.base_version = version;
|
|
78
106
|
});
|
|
79
107
|
written.push(PACKAGE_JSON);
|
|
80
108
|
printScaffoldWarnings(warnings, io);
|
|
@@ -82,5 +110,6 @@ export async function flowPull(argv, io) {
|
|
|
82
110
|
io.log(
|
|
83
111
|
`pulled ${flowId} v${version} from ${target} (${written.sort().join(', ')})`
|
|
84
112
|
);
|
|
113
|
+
if (removed.length) io.log(`removed ${removed.join(', ')}`);
|
|
85
114
|
return 0;
|
|
86
115
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { connect } from '../api.js';
|
|
2
2
|
import { readManifest, readManifestRaw, writeTypes } from '../project.js';
|
|
3
3
|
import { stampTypeFiles } from '../stamp.js';
|
|
4
|
-
import { resolveTarget } from '../target.js';
|
|
5
4
|
|
|
6
5
|
export const GEN_TYPES_USAGE = 'juno integration gen-types';
|
|
7
6
|
|
|
@@ -14,15 +13,9 @@ export const GEN_TYPES_USAGE = 'juno integration gen-types';
|
|
|
14
13
|
* manifest does, which is the right person to require it of.
|
|
15
14
|
*/
|
|
16
15
|
export async function genTypes(argv, io) {
|
|
17
|
-
const target = await
|
|
18
|
-
url: argv.url,
|
|
19
|
-
cwd: io.cwd,
|
|
20
|
-
env: io.env,
|
|
21
|
-
});
|
|
16
|
+
const { target, client } = await connect(argv, io);
|
|
22
17
|
const raw = await readManifestRaw(io.cwd);
|
|
23
18
|
const manifest = await readManifest(io.cwd);
|
|
24
|
-
const client =
|
|
25
|
-
argv.client ?? new JunoClient(target, { client: clientName(io.env) });
|
|
26
19
|
|
|
27
20
|
const generated = await client.typegen(manifest);
|
|
28
21
|
const written = await writeTypes(
|