@junoflow/cli 0.6.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 +189 -73
- 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 +182 -39
- 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 +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/check.js +31 -15
- package/src/commands/deploy.js +57 -4
- 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/pull.js +96 -102
- package/src/errors.js +3 -3
- package/src/exit.js +2 -2
- package/src/flow-project.js +47 -97
- package/src/index.js +62 -1
- package/src/project.js +76 -8
- package/src/watch.js +48 -11
- package/src/working-copy.js +262 -0
- package/tsconfig.page.json +15 -0
- package/verbs.json +51 -2
package/src/commands/pull.js
CHANGED
|
@@ -1,30 +1,47 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
import { promisify } from 'node:util';
|
|
5
|
-
|
|
6
1
|
import { clientName, JunoClient } from '../api.js';
|
|
7
2
|
import { UserError } from '../errors.js';
|
|
8
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
readIntegrationBaseVersion,
|
|
5
|
+
readManifest,
|
|
6
|
+
updateIntegrationPackage,
|
|
7
|
+
} from '../project.js';
|
|
8
|
+
import { stampTypeFiles } from '../stamp.js';
|
|
9
9
|
import { resolveTarget } from '../target.js';
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
baseArtifactPaths,
|
|
12
|
+
MANIFEST,
|
|
13
|
+
manifestText,
|
|
14
|
+
pullArtifacts,
|
|
15
|
+
requireCleanTree,
|
|
16
|
+
setDependencies,
|
|
17
|
+
writeChangedFiles,
|
|
18
|
+
} from '../working-copy.js';
|
|
19
|
+
import { pickTypeFiles } from './gen-types.js';
|
|
12
20
|
|
|
13
21
|
export const PULL_USAGE = 'juno integration pull [--force] [--id <id>]';
|
|
14
22
|
|
|
15
23
|
/**
|
|
16
|
-
* `juno integration pull` —
|
|
17
|
-
*
|
|
24
|
+
* `juno integration pull` — make the working copy's artifact files the
|
|
25
|
+
* install's active version: manifest.json and every file under src/ and
|
|
26
|
+
* test/, exactly as the version stores them, plus types/ regenerated and
|
|
27
|
+
* stamped against the pulled manifest, and package.json's
|
|
28
|
+
* `juno.base_version` (the version pulled, which the next check and deploy
|
|
29
|
+
* send).
|
|
18
30
|
*
|
|
19
31
|
* This exists because the zip is a good first touch and a bad second one: once
|
|
20
32
|
* the authoring agent has edited an integration you deployed, downloading the
|
|
21
33
|
* project again would clobber your working tree, while pull touches only the
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
34
|
+
* files Juno stores. The scaffold's script lines are not among them: they are
|
|
35
|
+
* project content, and an author's own edits to them are theirs — a fresh
|
|
36
|
+
* `juno integration init` is how to get the current ones. `dependencies` is
|
|
37
|
+
* replaced only when the pulled manifest declares some (an agent-authored
|
|
38
|
+
* integration); otherwise they live in package.json alone, which Juno does not
|
|
39
|
+
* store, and stay. Every other key in package.json, and of `juno`, stays.
|
|
40
|
+
*
|
|
41
|
+
* A local file is deleted only when the base version had it and the pulled
|
|
42
|
+
* one does not; a file neither has is one not deployed yet, and stays. It
|
|
43
|
+
* still refuses a dirty tree without --force, because an uncommitted edit is
|
|
44
|
+
* the one thing git cannot get back for you.
|
|
28
45
|
*/
|
|
29
46
|
export async function pull(argv, io) {
|
|
30
47
|
const target = await resolveTarget({
|
|
@@ -32,26 +49,26 @@ export async function pull(argv, io) {
|
|
|
32
49
|
cwd: io.cwd,
|
|
33
50
|
env: io.env,
|
|
34
51
|
});
|
|
35
|
-
|
|
52
|
+
let manifestId;
|
|
53
|
+
try {
|
|
54
|
+
manifestId = (await readManifest(io.cwd)).id;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
// With --id the manifest is about to be replaced, so a broken one is
|
|
57
|
+
// no reason to refuse.
|
|
58
|
+
if (argv.id === undefined) throw err;
|
|
59
|
+
}
|
|
60
|
+
const id = argv.id ?? manifestId;
|
|
36
61
|
if (!id) {
|
|
37
62
|
throw new UserError(
|
|
38
63
|
'manifest.json declares no id — pass --id <integration>'
|
|
39
64
|
);
|
|
40
65
|
}
|
|
41
66
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
);
|
|
48
|
-
} else if (dirty.length > 0) {
|
|
49
|
-
throw new UserError(
|
|
50
|
-
`working tree has uncommitted changes:\n${dirty.map((p) => ` ${p}`).join('\n')}\n` +
|
|
51
|
-
'Commit or stash them, or pass --force to overwrite.'
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
67
|
+
await requireCleanTree(io.cwd, {
|
|
68
|
+
force: argv.force,
|
|
69
|
+
overwrites: `${MANIFEST}, src/, test/ and types/`,
|
|
70
|
+
io,
|
|
71
|
+
});
|
|
55
72
|
|
|
56
73
|
const client =
|
|
57
74
|
argv.client ?? new JunoClient(target, { client: clientName(io.env) });
|
|
@@ -61,83 +78,60 @@ export async function pull(argv, io) {
|
|
|
61
78
|
`${id} has no active version on ${target} — nothing to pull`
|
|
62
79
|
);
|
|
63
80
|
}
|
|
64
|
-
//
|
|
65
|
-
// copy as it was.
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
// Everything is fetched before anything is written, so a failure leaves
|
|
82
|
+
// the working copy as it was.
|
|
83
|
+
const manifestRaw = manifestText(detail.manifest);
|
|
84
|
+
const stored = { [MANIFEST]: manifestRaw, ...detail.source_files };
|
|
85
|
+
const types = stampTypeFiles(
|
|
86
|
+
pickTypeFiles(await client.typegen(detail.manifest)),
|
|
87
|
+
manifestRaw
|
|
69
88
|
);
|
|
89
|
+
// The base belongs to the install the manifest names; pulling another
|
|
90
|
+
// (--id) has no base to diff against, so it deletes nothing.
|
|
91
|
+
const base = await baseArtifactPaths({
|
|
92
|
+
base:
|
|
93
|
+
id === manifestId ? await readIntegrationBaseVersion(io.cwd) : undefined,
|
|
94
|
+
version: detail.version,
|
|
95
|
+
stored,
|
|
96
|
+
read: async (v) => {
|
|
97
|
+
const b = await client.getIntegrationVersion(id, v);
|
|
98
|
+
return b ? [MANIFEST, ...Object.keys(b.SourceFiles ?? {})] : null;
|
|
99
|
+
},
|
|
100
|
+
id,
|
|
101
|
+
target,
|
|
102
|
+
io,
|
|
103
|
+
});
|
|
70
104
|
|
|
71
|
-
const written =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
105
|
+
const { written, removed } = await pullArtifacts(io.cwd, {
|
|
106
|
+
stored,
|
|
107
|
+
base,
|
|
108
|
+
kind: 'integration',
|
|
109
|
+
});
|
|
110
|
+
written.push(
|
|
111
|
+
...(await writeChangedFiles(
|
|
112
|
+
io.cwd,
|
|
113
|
+
Object.fromEntries(
|
|
114
|
+
Object.entries(types).map(([name, content]) => [
|
|
115
|
+
`types/${name}`,
|
|
116
|
+
content,
|
|
117
|
+
])
|
|
118
|
+
)
|
|
119
|
+
))
|
|
76
120
|
);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
121
|
+
const wrote = await updateIntegrationPackage(
|
|
122
|
+
io.cwd,
|
|
123
|
+
(pkg) => {
|
|
124
|
+
if (Object.keys(detail.manifest.dependencies ?? {}).length) {
|
|
125
|
+
setDependencies(pkg, detail.manifest);
|
|
126
|
+
}
|
|
127
|
+
pkg.juno.base_version = detail.version;
|
|
128
|
+
},
|
|
129
|
+
io
|
|
86
130
|
);
|
|
131
|
+
if (wrote) written.push('package.json');
|
|
87
132
|
io.log(
|
|
88
|
-
|
|
133
|
+
`pulled ${id} v${detail.version} from ${target} (${written.sort().join(', ')})`
|
|
89
134
|
);
|
|
135
|
+
if (removed.length) io.log(`removed ${removed.join(', ')}`);
|
|
90
136
|
return 0;
|
|
91
137
|
}
|
|
92
|
-
|
|
93
|
-
/** The `scripts` of the package.json a scaffold answered. */
|
|
94
|
-
function scaffoldScripts(files, target) {
|
|
95
|
-
try {
|
|
96
|
-
const scripts = JSON.parse(files['package.json']).scripts;
|
|
97
|
-
if (scripts && typeof scripts === 'object') return scripts;
|
|
98
|
-
} catch {
|
|
99
|
-
// Reported below.
|
|
100
|
-
}
|
|
101
|
-
throw new UserError(
|
|
102
|
-
`${target} answered a project with no readable package.json scripts`
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* replaceScripts sets package.json's `scripts`, keeping every other key and
|
|
108
|
-
* its order, written the way npm writes it. A missing or unreadable
|
|
109
|
-
* package.json is left alone with a warning: pull is not the place to invent
|
|
110
|
-
* one. Returns whether it wrote.
|
|
111
|
-
*/
|
|
112
|
-
async function replaceScripts(cwd, scripts, io) {
|
|
113
|
-
const path = join(cwd, 'package.json');
|
|
114
|
-
let pkg;
|
|
115
|
-
try {
|
|
116
|
-
pkg = JSON.parse(await readFile(path, 'utf8'));
|
|
117
|
-
} catch {
|
|
118
|
-
io.warn('no readable package.json — its scripts were not updated');
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
pkg.scripts = scripts;
|
|
122
|
-
await writeFile(path, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Returns the paths git reports as changed, [] for a clean tree, or null when
|
|
128
|
-
* this is not a git repository at all — three states the caller treats
|
|
129
|
-
* differently, so they must not collapse into a boolean.
|
|
130
|
-
*/
|
|
131
|
-
export async function dirtyPaths(cwd) {
|
|
132
|
-
try {
|
|
133
|
-
const { stdout } = await execFileAsync('git', ['status', '--porcelain'], {
|
|
134
|
-
cwd,
|
|
135
|
-
});
|
|
136
|
-
return stdout
|
|
137
|
-
.split('\n')
|
|
138
|
-
.map((line) => line.trim())
|
|
139
|
-
.filter(Boolean);
|
|
140
|
-
} catch {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
}
|
package/src/errors.js
CHANGED
|
@@ -52,9 +52,9 @@ export class UnreachableError extends Error {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* ConflictError is a deploy the server refused because the working
|
|
56
|
-
* stale: the flow's active version is not the
|
|
57
|
-
* was pulled from (a 409). The top-level handler prints the server's message,
|
|
55
|
+
* ConflictError is a deploy or check the server refused because the working
|
|
56
|
+
* copy is stale: the flow's, integration's or app's active version is not the
|
|
57
|
+
* one `juno.base_version` says it was pulled from (a 409). The top-level handler prints the server's message,
|
|
58
58
|
* which names both versions, and exits 4. The fix is `npm run pull`, or
|
|
59
59
|
* deploying with --force to replace whatever is active.
|
|
60
60
|
*/
|
package/src/exit.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* 2 usage: unknown command or flag, bad --input, a gateway 422 or 404,
|
|
9
9
|
* a bad --jq
|
|
10
10
|
* 3 the instance is unreachable (the fetch itself failed)
|
|
11
|
-
* 4 a deploy conflict:
|
|
12
|
-
* active version is not the
|
|
11
|
+
* 4 a deploy conflict: a working-copy deploy or check (any kind) got a
|
|
12
|
+
* 409 because the active version is not the copy's base_version
|
|
13
13
|
* 5 `juno ask` / `session approve`: the turn is still running (pending)
|
|
14
14
|
* 6 `juno ask` / `session approve`: the assistant is awaiting input
|
|
15
15
|
*/
|
package/src/flow-project.js
CHANGED
|
@@ -1,128 +1,78 @@
|
|
|
1
|
-
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
|
|
4
1
|
import { UserError } from './errors.js';
|
|
2
|
+
import { readSourceFiles, readTestFiles } from './project.js';
|
|
3
|
+
import {
|
|
4
|
+
baseVersionOf,
|
|
5
|
+
MANIFEST,
|
|
6
|
+
manifestText,
|
|
7
|
+
PACKAGE_JSON,
|
|
8
|
+
readBoundPackage,
|
|
9
|
+
readManifestFile,
|
|
10
|
+
typeFiles,
|
|
11
|
+
writeChangedFiles,
|
|
12
|
+
writePackageJSON,
|
|
13
|
+
} from './working-copy.js';
|
|
5
14
|
|
|
6
15
|
/**
|
|
7
|
-
* A flow working copy is
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
16
|
+
* A flow working copy is laid out as an integration's is: manifest.json, the
|
|
17
|
+
* sources under src/ (entry src/flow.ts) and the tests under test/ are the
|
|
18
|
+
* author's, types/ is
|
|
19
|
+
* generated, and package.json's `juno` block binds the directory to one flow.
|
|
20
|
+
* The layout is the server's (POST /api/flows/scaffold); this module only
|
|
21
|
+
* knows the handful of names a command reads or writes back, and what it
|
|
22
|
+
* shares with an app working copy lives in working-copy.js.
|
|
12
23
|
*/
|
|
13
|
-
export
|
|
14
|
-
export const
|
|
15
|
-
export const
|
|
24
|
+
export { MANIFEST, PACKAGE_JSON, manifestText, typeFiles };
|
|
25
|
+
export const SRC_DIR = 'src';
|
|
26
|
+
export const FLOW_ENTRY = 'src/flow.ts';
|
|
16
27
|
|
|
17
28
|
/**
|
|
18
29
|
* readFlowProject reads what ingest takes: the manifest (parsed, so a broken
|
|
19
|
-
* one fails here with its parse error rather than as a server 422),
|
|
20
|
-
* and the binding in package.json.
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
30
|
+
* one fails here with its parse error rather than as a server 422), every
|
|
31
|
+
* .ts and .json file under src/ and test/, and the binding in package.json.
|
|
32
|
+
* Juno stores test/ with the version and never compiles it. A copy
|
|
33
|
+
* with no src/flow.ts is refused before anything is sent. `flowId` is
|
|
34
|
+
* `juno.flow_id` — the id is identity, so the CLI sends it and the server
|
|
35
|
+
* refuses a manifest naming another — and `baseVersion` is
|
|
36
|
+
* `juno.base_version`, absent for a flow that has never been deployed from
|
|
37
|
+
* this copy.
|
|
24
38
|
*/
|
|
25
39
|
export async function readFlowProject(cwd) {
|
|
26
40
|
const pkg = await readFlowPackage(cwd);
|
|
41
|
+
const files = await readSourceFiles(cwd);
|
|
42
|
+
if (!Object.hasOwn(files, FLOW_ENTRY)) {
|
|
43
|
+
throw new UserError(
|
|
44
|
+
`no ${FLOW_ENTRY} — a flow working copy's entry is ${FLOW_ENTRY}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
27
47
|
return {
|
|
28
48
|
...(await readFlowManifest(cwd)),
|
|
29
|
-
|
|
49
|
+
files: { ...files, ...(await readTestFiles(cwd)) },
|
|
30
50
|
packageJSON: pkg,
|
|
31
51
|
flowId: pkg.juno.flow_id,
|
|
32
|
-
baseVersion:
|
|
33
|
-
? pkg.juno.base_version
|
|
34
|
-
: undefined,
|
|
52
|
+
baseVersion: baseVersionOf(pkg),
|
|
35
53
|
};
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
/** readFlowManifest is manifest.json, as bytes and parsed. */
|
|
39
|
-
export
|
|
40
|
-
const manifestRaw = await readText(cwd, MANIFEST);
|
|
41
|
-
try {
|
|
42
|
-
return { manifest: JSON.parse(manifestRaw), manifestRaw };
|
|
43
|
-
} catch (err) {
|
|
44
|
-
throw new UserError(`${MANIFEST} is not valid JSON: ${err.message}`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
57
|
+
export const readFlowManifest = readManifestFile;
|
|
47
58
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
* running one from the wrong place should say so rather than fail on a
|
|
52
|
-
* missing flow.ts.
|
|
53
|
-
*/
|
|
54
|
-
export async function readFlowPackage(cwd) {
|
|
55
|
-
let pkg = null;
|
|
56
|
-
try {
|
|
57
|
-
pkg = JSON.parse(await readFile(join(cwd, PACKAGE_JSON), 'utf8'));
|
|
58
|
-
} catch {
|
|
59
|
-
// Missing or unreadable: reported below as not a working copy.
|
|
60
|
-
}
|
|
61
|
-
if (typeof pkg?.juno?.flow_id !== 'string' || pkg.juno.flow_id === '') {
|
|
62
|
-
throw new UserError(
|
|
63
|
-
`${cwd} is not a flow working copy (no juno.flow_id in package.json) — ` +
|
|
64
|
-
'run `juno flow init <id>` to make one'
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
return pkg;
|
|
59
|
+
/** readFlowPackage is package.json, refusing a directory that is not a flow working copy. */
|
|
60
|
+
export function readFlowPackage(cwd) {
|
|
61
|
+
return readBoundPackage(cwd, { kind: 'flow', idKey: 'flow_id' });
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
/**
|
|
71
|
-
* updatePackageJSON rewrites package.json through
|
|
72
|
-
* it does not touch (and their order)
|
|
73
|
-
* newline — the way npm writes it, so the diff is only what changed.
|
|
65
|
+
* updatePackageJSON rewrites a flow working copy's package.json through
|
|
66
|
+
* `mutate`, keeping every key it does not touch (and their order).
|
|
74
67
|
*/
|
|
75
68
|
export async function updatePackageJSON(cwd, mutate) {
|
|
76
69
|
const pkg = await readFlowPackage(cwd);
|
|
77
70
|
mutate(pkg);
|
|
78
|
-
await
|
|
79
|
-
join(cwd, PACKAGE_JSON),
|
|
80
|
-
`${JSON.stringify(pkg, null, 2)}\n`,
|
|
81
|
-
'utf8'
|
|
82
|
-
);
|
|
71
|
+
await writePackageJSON(cwd, pkg);
|
|
83
72
|
}
|
|
84
73
|
|
|
85
74
|
/**
|
|
86
75
|
* writeFlowFiles writes files by working-copy name, skipping one whose
|
|
87
|
-
* content is already what is on disk
|
|
88
|
-
* changed nothing. Returns the names it wrote.
|
|
76
|
+
* content is already what is on disk. Returns the names it wrote.
|
|
89
77
|
*/
|
|
90
|
-
export
|
|
91
|
-
const written = [];
|
|
92
|
-
for (const [name, content] of Object.entries(files)) {
|
|
93
|
-
const path = join(cwd, name);
|
|
94
|
-
let current = null;
|
|
95
|
-
try {
|
|
96
|
-
current = await readFile(path, 'utf8');
|
|
97
|
-
} catch {
|
|
98
|
-
// Not there yet.
|
|
99
|
-
}
|
|
100
|
-
if (current === content) continue;
|
|
101
|
-
await writeFile(path, content, 'utf8');
|
|
102
|
-
written.push(name);
|
|
103
|
-
}
|
|
104
|
-
return written.sort();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** The canonical manifest a route answered, as manifest.json holds it. */
|
|
108
|
-
export function manifestText(manifest) {
|
|
109
|
-
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* The .d.ts files of a route's `types`. Filtering by extension rather than
|
|
114
|
-
* naming the two keeps a future third file working without a CLI release.
|
|
115
|
-
*/
|
|
116
|
-
export function typeFiles(types) {
|
|
117
|
-
return Object.fromEntries(
|
|
118
|
-
Object.entries(types ?? {}).filter(([name]) => name.endsWith('.d.ts'))
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function readText(cwd, name) {
|
|
123
|
-
try {
|
|
124
|
-
return await readFile(join(cwd, name), 'utf8');
|
|
125
|
-
} catch {
|
|
126
|
-
throw new UserError(`no ${name} in ${cwd}`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
78
|
+
export const writeFlowFiles = writeChangedFiles;
|
package/src/index.js
CHANGED
|
@@ -4,6 +4,12 @@ import {
|
|
|
4
4
|
AGENT_SKILL_INSTALL_USAGE,
|
|
5
5
|
agentSkillInstall,
|
|
6
6
|
} from './commands/agent-skill-install.js';
|
|
7
|
+
import { APP_CHECK_USAGE, appCheck } from './commands/app-check.js';
|
|
8
|
+
import { APP_DEPLOY_USAGE, appDeploy } from './commands/app-deploy.js';
|
|
9
|
+
import { APP_DEV_USAGE, appDev } from './commands/app-dev.js';
|
|
10
|
+
import { APP_GEN_TYPES_USAGE, appGenTypes } from './commands/app-gen-types.js';
|
|
11
|
+
import { APP_INIT_USAGE, appInit } from './commands/app-init.js';
|
|
12
|
+
import { APP_PULL_USAGE, appPull } from './commands/app-pull.js';
|
|
7
13
|
import { ASK_USAGE, ask } from './commands/ask.js';
|
|
8
14
|
import { CHECK_USAGE, check } from './commands/check.js';
|
|
9
15
|
import { CONFIG_SET_USAGE, configSet } from './commands/config.js';
|
|
@@ -18,6 +24,7 @@ import {
|
|
|
18
24
|
FLOW_GEN_TYPES_USAGE,
|
|
19
25
|
flowGenTypes,
|
|
20
26
|
} from './commands/flow-gen-types.js';
|
|
27
|
+
import { FLOW_FIXTURE_USAGE, flowFixture } from './commands/flow-fixture.js';
|
|
21
28
|
import { FLOW_INIT_USAGE, flowInit } from './commands/flow-init.js';
|
|
22
29
|
import { FLOW_PULL_USAGE, flowPull } from './commands/flow-pull.js';
|
|
23
30
|
import { GEN_TYPES_USAGE, genTypes } from './commands/gen-types.js';
|
|
@@ -151,6 +158,15 @@ const WORKING_COPY = {
|
|
|
151
158
|
options: { ...URL_OPTION, force: { type: 'boolean', default: false } },
|
|
152
159
|
usage: FLOW_PULL_USAGE,
|
|
153
160
|
},
|
|
161
|
+
'flow fixture': {
|
|
162
|
+
run: flowFixture,
|
|
163
|
+
options: {
|
|
164
|
+
...URL_OPTION,
|
|
165
|
+
run: { type: 'string' },
|
|
166
|
+
force: { type: 'boolean', default: false },
|
|
167
|
+
},
|
|
168
|
+
usage: FLOW_FIXTURE_USAGE,
|
|
169
|
+
},
|
|
154
170
|
'integration init': {
|
|
155
171
|
run: init,
|
|
156
172
|
options: {
|
|
@@ -189,6 +205,7 @@ const WORKING_COPY = {
|
|
|
189
205
|
...URL_OPTION,
|
|
190
206
|
id: { type: 'string' },
|
|
191
207
|
watch: { type: 'boolean', default: false },
|
|
208
|
+
force: { type: 'boolean', default: false },
|
|
192
209
|
json: { type: 'boolean', default: false },
|
|
193
210
|
},
|
|
194
211
|
usage: DEPLOY_USAGE,
|
|
@@ -202,6 +219,50 @@ const WORKING_COPY = {
|
|
|
202
219
|
},
|
|
203
220
|
usage: PULL_USAGE,
|
|
204
221
|
},
|
|
222
|
+
'app init': {
|
|
223
|
+
run: appInit,
|
|
224
|
+
options: { ...URL_OPTION, here: { type: 'boolean', default: false } },
|
|
225
|
+
usage: APP_INIT_USAGE,
|
|
226
|
+
positional: true,
|
|
227
|
+
},
|
|
228
|
+
'app gen-types': {
|
|
229
|
+
run: appGenTypes,
|
|
230
|
+
options: URL_OPTION,
|
|
231
|
+
usage: APP_GEN_TYPES_USAGE,
|
|
232
|
+
},
|
|
233
|
+
'app check': {
|
|
234
|
+
run: appCheck,
|
|
235
|
+
options: {
|
|
236
|
+
...URL_OPTION,
|
|
237
|
+
'skip-local': { type: 'boolean', default: false },
|
|
238
|
+
json: { type: 'boolean', default: false },
|
|
239
|
+
},
|
|
240
|
+
usage: APP_CHECK_USAGE,
|
|
241
|
+
},
|
|
242
|
+
'app deploy': {
|
|
243
|
+
run: appDeploy,
|
|
244
|
+
options: {
|
|
245
|
+
...URL_OPTION,
|
|
246
|
+
watch: { type: 'boolean', default: false },
|
|
247
|
+
force: { type: 'boolean', default: false },
|
|
248
|
+
json: { type: 'boolean', default: false },
|
|
249
|
+
},
|
|
250
|
+
usage: APP_DEPLOY_USAGE,
|
|
251
|
+
},
|
|
252
|
+
'app pull': {
|
|
253
|
+
run: appPull,
|
|
254
|
+
options: { ...URL_OPTION, force: { type: 'boolean', default: false } },
|
|
255
|
+
usage: APP_PULL_USAGE,
|
|
256
|
+
},
|
|
257
|
+
// No --url: the dev server is offline and never reads juno.target.
|
|
258
|
+
'app dev': {
|
|
259
|
+
run: appDev,
|
|
260
|
+
options: {
|
|
261
|
+
port: { type: 'string' },
|
|
262
|
+
scenario: { type: 'string' },
|
|
263
|
+
},
|
|
264
|
+
usage: APP_DEV_USAGE,
|
|
265
|
+
},
|
|
205
266
|
};
|
|
206
267
|
|
|
207
268
|
/** The handler of a non-tool row, working-copy or not, or undefined. */
|
|
@@ -223,7 +284,7 @@ function dispatchable(command) {
|
|
|
223
284
|
return row && DISPATCHABLE.includes(row) ? row : undefined;
|
|
224
285
|
}
|
|
225
286
|
|
|
226
|
-
const USAGE = `juno — operate a Juno instance, and develop flows and
|
|
287
|
+
const USAGE = `juno — operate a Juno instance, and develop flows, integrations and apps for it
|
|
227
288
|
|
|
228
289
|
Usage: juno <command> [options]
|
|
229
290
|
juno <group> <verb> [id] [--input <json>|-|@file] [--jq <expr>]
|
package/src/project.js
CHANGED
|
@@ -2,15 +2,22 @@ import { readFile, readdir, mkdir, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import { dirname, join, posix, relative, sep } from 'node:path';
|
|
3
3
|
|
|
4
4
|
import { UserError } from './errors.js';
|
|
5
|
+
import { PACKAGE_JSON, writePackageJSON } from './working-copy.js';
|
|
5
6
|
|
|
6
7
|
/** Only these reach the server. Everything else is project furniture. */
|
|
7
8
|
const SOURCE_EXTENSIONS = ['.ts', '.json'];
|
|
8
9
|
const SOURCE_DIR = 'src';
|
|
10
|
+
const TEST_DIR = 'test';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
|
-
* Read the four things Juno looks at: manifest.json, the sources under src
|
|
12
|
-
* package.json's dependencies, and the lockfile.
|
|
13
|
-
*
|
|
13
|
+
* Read the four things Juno looks at: manifest.json, the sources under src/
|
|
14
|
+
* and test/, package.json's dependencies, and the lockfile. test/ is stored
|
|
15
|
+
* with the version but never compiled, linted or run server-side; types/ is
|
|
16
|
+
* generated and never uploaded.
|
|
17
|
+
*
|
|
18
|
+
* `baseVersion` is package.json's `juno.base_version`, the version this copy
|
|
19
|
+
* was pulled from or last deployed as, or undefined when it records none (a
|
|
20
|
+
* copy made before the check existed, or one with no `juno` block at all).
|
|
14
21
|
*/
|
|
15
22
|
export async function readProject(cwd = process.cwd()) {
|
|
16
23
|
const manifest = await readManifest(cwd);
|
|
@@ -18,14 +25,61 @@ export async function readProject(cwd = process.cwd()) {
|
|
|
18
25
|
if (Object.keys(files).length === 0) {
|
|
19
26
|
throw new UserError(`no TypeScript sources found under ${SOURCE_DIR}/`);
|
|
20
27
|
}
|
|
28
|
+
const packageJSON = await readOptional(join(cwd, PACKAGE_JSON));
|
|
21
29
|
return {
|
|
22
30
|
manifest,
|
|
23
|
-
files,
|
|
24
|
-
packageJSON
|
|
31
|
+
files: { ...files, ...(await readTestFiles(cwd)) },
|
|
32
|
+
packageJSON,
|
|
25
33
|
lockfile: await readOptional(join(cwd, 'package-lock.json')),
|
|
34
|
+
baseVersion: baseVersionOfText(packageJSON),
|
|
26
35
|
};
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
/**
|
|
39
|
+
* readIntegrationBaseVersion is package.json's `juno.base_version`, or
|
|
40
|
+
* undefined: what `pull` diffs against without reading the sources.
|
|
41
|
+
*/
|
|
42
|
+
export async function readIntegrationBaseVersion(cwd) {
|
|
43
|
+
return baseVersionOfText(await readOptional(join(cwd, PACKAGE_JSON)));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** `juno.base_version` of package.json's text, or undefined. */
|
|
47
|
+
function baseVersionOfText(text) {
|
|
48
|
+
let pkg;
|
|
49
|
+
try {
|
|
50
|
+
pkg = JSON.parse(text);
|
|
51
|
+
} catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const base = pkg?.juno?.base_version;
|
|
55
|
+
return Number.isInteger(base) ? base : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* updateIntegrationPackage rewrites package.json through `mutate`, keeping
|
|
60
|
+
* every key it does not touch (and their order). `juno` is handed over as an
|
|
61
|
+
* object even when the copy has none yet: an integration copy carries one only
|
|
62
|
+
* once it records a target or a base. A missing or unreadable package.json is
|
|
63
|
+
* left alone with a warning — no command here is the place to invent one.
|
|
64
|
+
* Returns whether it wrote.
|
|
65
|
+
*/
|
|
66
|
+
export async function updateIntegrationPackage(cwd, mutate, io) {
|
|
67
|
+
let pkg;
|
|
68
|
+
try {
|
|
69
|
+
pkg = JSON.parse(await readFile(join(cwd, PACKAGE_JSON), 'utf8'));
|
|
70
|
+
} catch {
|
|
71
|
+
pkg = null;
|
|
72
|
+
}
|
|
73
|
+
if (pkg === null || typeof pkg !== 'object' || Array.isArray(pkg)) {
|
|
74
|
+
io.warn(`no readable ${PACKAGE_JSON} — it was not updated`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (pkg.juno === null || typeof pkg.juno !== 'object') pkg.juno = {};
|
|
78
|
+
mutate(pkg);
|
|
79
|
+
await writePackageJSON(cwd, pkg);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
29
83
|
/**
|
|
30
84
|
* The manifest as it sits on disk. The stamp in types/integration-env.d.ts
|
|
31
85
|
* fingerprints these exact bytes, so both writing and verifying it go through
|
|
@@ -51,15 +105,29 @@ export async function readManifest(cwd = process.cwd()) {
|
|
|
51
105
|
}
|
|
52
106
|
|
|
53
107
|
/** Walks src/ and returns repo-relative POSIX paths → content. */
|
|
54
|
-
export
|
|
55
|
-
|
|
108
|
+
export function readSourceFiles(cwd = process.cwd()) {
|
|
109
|
+
return readSourceTree(cwd, SOURCE_DIR);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* readTestFiles is readSourceFiles for test/: the same .ts and .json rule,
|
|
114
|
+
* because Juno stores tests beside the sources under the same path rules.
|
|
115
|
+
* Every kind's working copy sends them; none requires them.
|
|
116
|
+
*/
|
|
117
|
+
export function readTestFiles(cwd = process.cwd()) {
|
|
118
|
+
return readSourceTree(cwd, TEST_DIR);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** readSourceTree walks dir for .ts and .json files, by repo-relative POSIX path. */
|
|
122
|
+
async function readSourceTree(cwd, dir) {
|
|
123
|
+
const root = join(cwd, dir);
|
|
56
124
|
const files = {};
|
|
57
125
|
async function walk(dir) {
|
|
58
126
|
let entries;
|
|
59
127
|
try {
|
|
60
128
|
entries = await readdir(dir, { withFileTypes: true });
|
|
61
129
|
} catch {
|
|
62
|
-
return; // no
|
|
130
|
+
return; // no such directory at all; the caller reports a missing src/
|
|
63
131
|
}
|
|
64
132
|
for (const entry of entries) {
|
|
65
133
|
const full = join(dir, entry.name);
|