@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 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 remote = Git.normalizeRepoUrl(await Git.remoteUrl(root));
69
- const matches = remote ? services.filter((s) => Git.normalizeRepoUrl(s.repoUrl) === remote) : [];
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} (no linked folder here — clone ${b.cloneUrl || 'the repo'} and run \`motionode link\`)`);
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
@@ -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.0",
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": {