@motionode/cli 0.2.0 → 0.2.1
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/bin/motionode.js +7 -0
- package/lib/bridge.js +13 -0
- package/lib/commands.js +37 -4
- package/lib/git.js +0 -0
- package/lib/git_credential.js +19 -1
- package/package.json +1 -1
package/bin/motionode.js
CHANGED
|
@@ -64,6 +64,13 @@ function flags(argv) {
|
|
|
64
64
|
async function main() {
|
|
65
65
|
const [command, ...rest] = process.argv.slice(2);
|
|
66
66
|
const args = flags(rest);
|
|
67
|
+
/**
|
|
68
|
+
* A server named for `connect` is the one this computer keeps using: the work
|
|
69
|
+
* panel prints `motionode connect --api …` once, and every command after it
|
|
70
|
+
* must stay on that Motionode rather than fall back to production
|
|
71
|
+
* (QA-20261023T234000Z).
|
|
72
|
+
*/
|
|
73
|
+
if (command === 'connect' && typeof args.api === 'string') Config.useApi(args.api);
|
|
67
74
|
// --api (or MOTIONODE_API_URL) picks the Motionode — and its own saved connection.
|
|
68
75
|
const config = Config.load(typeof args.api === 'string' ? args.api : null);
|
|
69
76
|
switch (command) {
|
package/lib/bridge.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const Git = require('./git');
|
|
12
|
+
const { worktreePath } = require('./commands');
|
|
12
13
|
const { client } = require('./api');
|
|
13
14
|
const { version } = require('./api');
|
|
14
15
|
|
|
@@ -51,6 +52,18 @@ async function runJob(config, job) {
|
|
|
51
52
|
const ws = workspaceFor(config, payload.serviceUuid);
|
|
52
53
|
if (!ws) throw new Error(`No folder on this computer is linked to ${payload.serviceName || 'that repository'}. Run \`motionode link\` in it.`);
|
|
53
54
|
if (job.kind === 'checkout') {
|
|
55
|
+
/**
|
|
56
|
+
* In --worktrees mode `motionode work` owns the ticket's branch in its own
|
|
57
|
+
* folder, so checking it out in the linked folder conflicts with that
|
|
58
|
+
* worktree (QA-20261023T235400Z). Create/reuse the same folder instead.
|
|
59
|
+
*/
|
|
60
|
+
if (ws.worktrees && payload.ticketDisplayId) {
|
|
61
|
+
const dir = worktreePath(ws, payload.ticketDisplayId);
|
|
62
|
+
const wt = await Git.worktreeFor(ws.path, payload.branch, dir);
|
|
63
|
+
log(`checked out ${payload.branch} in ${wt.path}`);
|
|
64
|
+
const st = await Git.status(wt.path);
|
|
65
|
+
return { branch: st.branch, headSha: st.headSha, path: wt.path };
|
|
66
|
+
}
|
|
54
67
|
const st = await Git.checkout(ws.path, payload.branch);
|
|
55
68
|
log(`checked out ${payload.branch} in ${ws.path}`);
|
|
56
69
|
return { branch: st.branch, headSha: st.headSha };
|
package/lib/commands.js
CHANGED
|
@@ -65,8 +65,26 @@ async function link(config, { serviceUuid = null, dir = process.cwd(), worktrees
|
|
|
65
65
|
if (!services.length) throw new Error('You have no projects with AI building enabled to work on locally.');
|
|
66
66
|
let pick = serviceUuid ? services.find((s) => s.uuid === serviceUuid) : null;
|
|
67
67
|
if (!pick) {
|
|
68
|
-
const
|
|
69
|
-
const
|
|
68
|
+
const remoteUrl = await Git.remoteUrl(root);
|
|
69
|
+
const remote = Git.normalizeRepoUrl(remoteUrl);
|
|
70
|
+
/**
|
|
71
|
+
* A clone made with `motionode clone` has this Motionode's git proxy as its
|
|
72
|
+
* remote, not GitHub's URL; recognize the repository behind it instead of
|
|
73
|
+
* asking the person to choose what they just cloned
|
|
74
|
+
* (QA-20261023T234000Z).
|
|
75
|
+
*/
|
|
76
|
+
const { proxiedRepoOf, ownerRepoOf } = require('./git_credential');
|
|
77
|
+
const proxied = proxiedRepoOf(config, remoteUrl);
|
|
78
|
+
const sameRepo = (repoUrl) => {
|
|
79
|
+
if (!proxied || !repoUrl) return false;
|
|
80
|
+
const ref = ownerRepoOf(repoUrl);
|
|
81
|
+
return !!ref
|
|
82
|
+
&& ref.owner.toLowerCase() === proxied.owner.toLowerCase()
|
|
83
|
+
&& ref.repo.toLowerCase() === proxied.repo.toLowerCase();
|
|
84
|
+
};
|
|
85
|
+
const matches = remote
|
|
86
|
+
? services.filter((s) => Git.normalizeRepoUrl(s.repoUrl) === remote || sameRepo(s.repoUrl))
|
|
87
|
+
: [];
|
|
70
88
|
if (matches.length === 1) pick = matches[0];
|
|
71
89
|
else {
|
|
72
90
|
const list = matches.length ? matches : services;
|
|
@@ -146,6 +164,21 @@ function worktreePath(ws, ticketRef) {
|
|
|
146
164
|
return path.join(path.dirname(ws.path), `${path.basename(ws.path)}.worktrees`, String(ticketRef).replace(/[^A-Za-z0-9._-]/g, '_'));
|
|
147
165
|
}
|
|
148
166
|
|
|
167
|
+
/**
|
|
168
|
+
* How to get a service whose repository has no linked folder here. A Motionode-hosted
|
|
169
|
+
* repository is private to Motionode, so its GitHub URL is not a clone the person can use:
|
|
170
|
+
* clone it through Motionode, against the server this command is already talking to.
|
|
171
|
+
*/
|
|
172
|
+
function cloneGuidance(b) {
|
|
173
|
+
if (b?.motionodeHosted) {
|
|
174
|
+
return b.repo
|
|
175
|
+
? `no linked folder here — clone it through Motionode: \`motionode clone ${b.repo}\`, then run \`motionode link\``
|
|
176
|
+
: 'no linked folder here — clone it through Motionode with `motionode clone <owner/repo>`, then run `motionode link`';
|
|
177
|
+
}
|
|
178
|
+
if (b?.cloneUrl) return `no linked folder here — \`git clone ${b.cloneUrl}\`, then run \`motionode link\``;
|
|
179
|
+
return 'no linked folder here — clone the repository, then run `motionode link`';
|
|
180
|
+
}
|
|
181
|
+
|
|
149
182
|
/** What `motionode finish` tells the person was recorded (the same facts the web confirmation shows). */
|
|
150
183
|
function finishLines(res) {
|
|
151
184
|
const lines = [`✓ ${res.summary || 'Build finished.'}`];
|
|
@@ -166,7 +199,7 @@ async function work(config, ref) {
|
|
|
166
199
|
for (const b of lw.branches || []) {
|
|
167
200
|
const ws = (config.workspaces || []).find((w) => w.serviceUuid === b.serviceUuid);
|
|
168
201
|
if (!ws) {
|
|
169
|
-
console.log(` ${b.serviceName}: ${b.branch} (
|
|
202
|
+
console.log(` ${b.serviceName}: ${b.branch} (${cloneGuidance(b)})`);
|
|
170
203
|
continue;
|
|
171
204
|
}
|
|
172
205
|
try {
|
|
@@ -210,4 +243,4 @@ async function logout(config) {
|
|
|
210
243
|
console.log('✓ Disconnected this computer. (Remove it in Motionode Settings → Development workflow to revoke it there too.)');
|
|
211
244
|
}
|
|
212
245
|
|
|
213
|
-
module.exports = { login, link, connect, unlink, status, work, finish, logout, worktreePath, finishLines };
|
|
246
|
+
module.exports = { login, link, connect, unlink, status, work, finish, logout, worktreePath, finishLines, cloneGuidance };
|
package/lib/git.js
CHANGED
|
File without changes
|
package/lib/git_credential.js
CHANGED
|
@@ -76,6 +76,24 @@ function ownerRepoOf(value) {
|
|
|
76
76
|
return m ? { owner: m[1], repo: m[2] } : null;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* owner/repo behind a Motionode git-proxy URL on this Motionode
|
|
81
|
+
* (`<api>/git/<owner>/<repo>.git`), or null for any other remote. The clone
|
|
82
|
+
* `motionode clone` makes has this remote, and `motionode link` needs the
|
|
83
|
+
* repository it stands for (QA-20261023T234000Z).
|
|
84
|
+
*/
|
|
85
|
+
function proxiedRepoOf(config, remoteUrl) {
|
|
86
|
+
try {
|
|
87
|
+
const remote = new URL(String(remoteUrl || ''));
|
|
88
|
+
const api = new URL(String(config?.apiUrl || ''));
|
|
89
|
+
if (remote.host.toLowerCase() !== api.host.toLowerCase()) return null;
|
|
90
|
+
const m = remote.pathname.match(/^\/git\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
|
91
|
+
return m ? { owner: decodeURIComponent(m[1]), repo: decodeURIComponent(m[2]) } : null;
|
|
92
|
+
} catch (_e) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
79
97
|
/** The git config that makes this CLI the credential helper for one folder (never global). */
|
|
80
98
|
function helperConfig(config) {
|
|
81
99
|
const bin = path.resolve(__dirname, '..', 'bin', 'motionode.js');
|
|
@@ -131,4 +149,4 @@ async function clone(config, target, dir = null) {
|
|
|
131
149
|
return null;
|
|
132
150
|
}
|
|
133
151
|
|
|
134
|
-
module.exports = { helper, clone, parseCredentialInput, isForThisMotionode, proxyUrl, ownerRepoOf, helperConfig };
|
|
152
|
+
module.exports = { helper, clone, parseCredentialInput, isForThisMotionode, proxyUrl, ownerRepoOf, proxiedRepoOf, helperConfig };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@motionode/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Connect your local repositories to Motionode: work tickets in Cursor, VS Code or any editor while Motionode keeps the work tied to the ticket, its git branches, verification and delivery.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"bin": {
|